Gorm Draft

This commit is contained in:
Tyrel Souza 2022-10-17 17:23:07 -04:00
parent 5bd8eef77d
commit e24a223ee1
No known key found for this signature in database
GPG Key ID: F6582CF1308A2360

View File

@ -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 <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 <https://blog.logrocket.com/how-to-build-a-rest-api-with-golang-using-gin-and-gorm/>`_ 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 <https://github.com/wetterj/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.