diff --git a/content/blog/2022-10-17_comparing-go-gorm-and-sqlx.rst b/content/blog/2022-10-17_comparing-go-gorm-and-sqlx.rst index 2662937..a5c495b 100644 --- a/content/blog/2022-10-17_comparing-go-gorm-and-sqlx.rst +++ b/content/blog/2022-10-17_comparing-go-gorm-and-sqlx.rst @@ -46,3 +46,128 @@ Being that I come from the Django (and a few years of ActiveRecord) land, I reac If you want to skip directly to the source, check out `https://gitea.tyrel.dev/tyrel/go-webservice-gin `_. Full design disclosure: I followed a couple of blog posts in order to develop this, so it is in the form explictly decided upon by the `logrocket blog post `_ and may not be the most efficient way to organize the module. +In order to instantiate a model definition, it's pretty easy. +What I did is make a new package called ``models`` and inside made a file for my Album. + +.. code:: go + + type Album struct { + ID string `json:"id" gorm:"primary_key"` + Title string `json:"title"` + Artist string `json:"artist"` + Price float64 `json:"price"` + } + +This tracks with how I would do the same for any other kind of struct in Go, so this wasn't too difficult to do. +What was kind of annoying was that I had to also make some structs for Creating the album and Updating the Album, this felt like duplicated effort that might have been better served with some composition. + +I would have structured the controllers differently, but that may be a Gin thing and how it takes points to functions, vs pointers to receivers on a struct. +Not specific to GORM. +Each of the controller functions were bound to a ``gin.Context`` pointer, rather than receivers on an AlbumController struct. + +The ``FindAlbum`` controller was simple: + +.. code:: go + + func FindAlbum(c *gin.Context) { + var album models.Album + if err := models.DB.Where("id = ?", c.Param("id")).First(&album).Error; err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"}) + } + c.JSON(http.StatusOK, gin.H{"data": album}) + } + +Which will take in a ``/:id`` path parameter, and the GORM part of this is the third line there. + +.. code:: go + + models.DB.Where("id = ?", c.Param("id")).First(&album).Error + +To run a select, you chain a ``Where`` on the DB (which is the connection here) and it will build up your query. +If you want to do joins, this is where you would chain ``.Joins`` etc... +You then pass in your album variable to bind the result to the struct, and if there's no errors, you continue on with the bound variable. +Error handling is standard Go logic, ``if err != nil`` etc and then pass that into your API of choice (Gin here) error handler. + +This was really easy to set up, and if you want to get a slice back you just use ``DB.Find`` instead, and bind to a slice of those structs. + +.. code:: go + + var albums []models.Album + models.DB.Find(&albums) + + +SQLX +~~~~ + +SQLX is a bit different, as it's not an ORM, it's extensions in Go to query with SQL, but still a good pattern for abstracting away your SQL to some dark corner of the app and not inline everywhere. +For this I didn't follow someone's blog post — I had a grasp on how to use Gin pretty okay by now and essentially copied someone elses repo with my existing model. +`gin-sqlx-crud `_. + +This one set up a bit wider of a structure, with deeper nested packages. +Inside my ``internal`` folder there's ``controllers``, ``forms``, ``models/sql``, and ``server``. +I'll only bother describing the ``models`` package here, as thats the SQLX part of it. + +In the ``models/album.go`` file, there's your standard struct here, but this time its bound to ``db`` not ``json``, I didn't look too deep yet but I presume that also forces the columns to set the json name. + +.. code:: go + + type Album struct { + ID int64 `db:"id"` + Title string `db:"title"` + Artist string `db:"artist"` + Price float64 `db:"price"` + } + +An interface to make a service, and a receiver are made for applying the ``CreateAlbum`` form (in another package) which sets the form name and json name in it. + +.. code:: go + + func (a *Album) ApplyForm(form *forms.CreateAlbum) { + a.ID = *form.ID + a.Title = *form.Title + a.Artist = *form.Artist + a.Price = *form.Price + } + +So there's the receiver action I wanted at least! + +Nested inside the ``models/sql/album.go`` file and package, is all of the Receiver code for the service. +I'll just comment the smallest one, as that gets my point across. +Here is where the main part of GORM/SQLX differ - raw SQL shows up. + +.. code:: go + + func (s *AlbumService) GetAll() (*[]models2.Album, error) { + q := `SELECT * FROM albums;` + + var output []models2.Album + err := s.conn.Select(&output, q) + // Replace the SQL error with our own error type. + if err == sql.ErrNoRows { + return nil, models2.ErrNotFound + } else if err != nil { + return nil, err + } else { + return &output, nil + } + } + +This will return a slice of Albums - but if you notice on the second line, you have to write your own queries. +A little bit more in control of how things happen, with a ``SELECT * ...`` vs the gorm ``DB.Find`` style. + +To me this feels more like using ``pymysql``, in fact its a very similar process. +You use the ``service.connection.Get`` and pass in what you want the output bound to, the string query, and any parameters. +This feels kind of backwards to me - I'd much rather have the order be: query, bound, parameters, but thats what they decided for their order. + +Conclusion +~~~~~~~~~~ + +Overall, both were pretty easy to set up for one model. +Given the choice I would look at who the source code is written for. +If you're someone who knows a lot of SQL, then SQLX is fine. +If you like abstractions, and more of a "Code as Query" style, then GORM is probably the best of these two options. + +I will point out that GORM does more than just "query and insert" there is migration, logging, locking, dry run mode, and more. +If you want to have a full fledged system, that might be a little heavy, then GORM is the right choice. + +SQLX is great if what you care about is marshalling, and a very quick integration into any existing codebase.