https://github.com/ethanbaker/sql-wrapper
An SQL Wrapper for Structs in Golang
https://github.com/ethanbaker/sql-wrapper
Last synced: about 1 month ago
JSON representation
An SQL Wrapper for Structs in Golang
- Host: GitHub
- URL: https://github.com/ethanbaker/sql-wrapper
- Owner: ethanbaker
- License: apache-2.0
- Created: 2024-01-19T15:53:13.000Z (over 2 years ago)
- Default Branch: main
- Last Pushed: 2024-03-18T19:26:02.000Z (about 2 years ago)
- Last Synced: 2025-01-16T04:12:41.169Z (over 1 year ago)
- Language: Go
- Size: 148 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 2
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README

[![Contributors][contributors-shield]][contributors-url]
[![Forks][forks-shield]][forks-url]
[![Stargazers][stars-shield]][stars-url]
[![Issues][issues-shield]][issues-url]
[![License][license-shield]][license-url]
[![LinkedIn][linkedin-shield]][linkedin-url]
[](https://godoc.org/github.com/ethanbaker/sql-wrapper)
[](https://goreportcard.com/report/github.com/ethanbaker/sql-wrapper)
[](./docs/go-coverage.svg)
Table of Contents
## About
This project is used to wrap golang structs in a schema that makes it easier to repeatedly save occurances of those structs to SQL.
The motivation of this project is to avoid the annoying task of updating SQL saving in golang projects. For example, if an API is saving `Record` structs to an SQL database, and then spec changes require the `Record` struct to change, it can be very cumbersome to go through all of the manually-written SQL code to change this.
In addition, wrapping SQL structs makes set up much, much easier. You only need to worry about the initialization of your structs and related SQL instead of every SQL action.
### Limitations
This project has a few key limitations:
* You must define all of your SQL types in the struct using tags
* The wrapper attempts to match these automatically, but it trusts SQL to make decisions and throw errors. If you declare a field as an integer when it is actually a string, SQL will handle it
* You must make your struct a part of the `Readable` interface
* This is required because I have no idea how to automate reading SQL tables with an indeterminate amount of interface pointers (if you know how to do this please consider contributing!)
* There are some "template" `Read` methods in the `examples` directory for different scenarios that you can check out
* Not every struct attribute is supported
* So far, only primitive types (`string`, `int`, `enums`, etc), pointers (`*Object`), and lists of pointers (`[]*Object`) are supported
* Maps are **not** supported
### Built With
* [Golang](https://go.dev/)
* SQL
## Getting Started
This wrapper is relatively easy to set up for your projects. These steps will follow along the `examples/record` project found in the repository.
#### Struct Setup
For each struct you want to wrap, define the struct and the SQL table it represents. At the moment this is a relatively manual process done with struct tags.
* The `sql` tag tells the wrapper to include this field for SQL consideration. The value of this field is the name of the associated SQL column
* The `def` tag tells the wrapper how to initialize this field as a column in SQL.
You do not need to create an ID field; one will be added automatically.
```go
type Record struct {
Author string `sql:"Author" def:"VARCHAR(128)"`
Likes int `sql:"Likes" def:"INT"`
Type PostType `sql:"Type" def:"ENUM('Original', 'Comment', 'Repost')"`
}
```
In this instance, a `Record` struct is created with three different fields (the `Type` field has an enum value defined outside of this snippet). Then, each field has related tags to fit with the definition.
When the wrapper is initialized, an SQL table will be created that looks like the following:
|id |Author|Likes|Type|
|---|------|-----|----|
|...| | | |
Each column has the following SQL type:
* **id**: `INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY` (this is automatically generated for every struct)
* **Author**: `VARCHAR(128)`
* **Likes**: `INT`
* **Type**: `ENUM('Original', 'Comment', 'Repost')`
#### Foreign Relations
Foreign relations are supported for wrappers. Foreign relations work by relating a **source** (the struct you are defining) and a **target** (the struct you are referencing).
There are four different types stored as the `RelationType` enum:
* `OneToOne`: Match one source object to one target object. Objects cannot be linked to more than one other object.
* Relations are done using a *pointer* to the other object
* This is equivalent to `ManyToOne` with a `UNIQUE` constraint on the target ID
* `ManyToOne`: Match any amount of source objects to one target object. Source objects cannot be linked to more than one other object.
* Relations are done using a *pointer* to the other object
* `OneToMany`: Match any amount of target objects to one source object. Target objects cannot be linked to more than one source object.
* Relations are done using an *array of pointers* to the other object(s)
* This is equivalent to `ManyToMany` with a `UNIQUE` constraint on the target ID
* `ManyToMany`: Match any amount of target objects to any source object.
* Relations are done using an *array of pointers* to the other object(s)
You can define struct attributes to be a foreign relation by adding a `rel` field to the attribute tag. For example:
```go
type ReferenceObject struct {
OneToOne *Object1 `sql:"Object1ID" rel:"one-to-one"`
ManyToOne *Object2 `sql:"Object2ID" rel:"many-to-one"`
OneToMany []*Object3 `sql:"Object3ID" rel:"one-to-many"`
ManyToMany []*Object4 `sql:"Object4ID" rel:"many-to-many"`
}
```
You can see examples of foreign relations in the `examples` folder of this project. The examples that deal with foreign relations are:
* `examples/user-post`: a one-to-many relationship between User and Post, where a User can have a list of Posts
* `examples/item-identification`: a one-to-one relationship between Item and Identification. An Item has one and only one Identification struct created and linked to it
#### Read Method
Next, a `Read` function needs to be created that is attached to the struct.
Because SQL querires need specificity when reading in new values, this is done easiest through a user-defined function.
If you know of a way to make reading in SQL tables easier, please consider [contributing](#contributing)!
```go
// Read function reads in values from an SQL database
// NOTE: there is no pointer receiver in this method to properly match the Readable interface
func (r Record) Read(db *sql.DB) (map[int]sql_wrapper.Readable, error) {
// Create a list of Readable objects to populate
items := map[int]sql_wrapper.Readable{}
// Get the main elements
// NOTE: the name of the SQl table you want to use is the name of the struct
// NOTE: the commented line below should be equivalent:
// rows, err := db.Query("SELECT * FROM " + reflect.TypeOf(s.template).Name())
rows, err := db.Query("SELECT * FROM Record")
if err != nil {
return items, err
}
defer rows.Close()
// Read each row in the query
var (
id int
author string
likes int
t PostType
)
for rows.Next() {
// Scan the specific elements of this struct in to custom variables
if err := rows.Scan(&id, &author, &likes, &t); err != nil {
return items, err
}
// Create a new Record object and add it to our list
// NOTE: you **MUST** add the object as a pointer. If not, then the object will be stored and retreived as a copy, leading to undesired behavior
obj := Record{Author: author, Likes: likes, Type: t}
items[id] = &obj
}
return items, nil
}
```
As noted in the code snippet, there are many important notes to keep in mind:
* The function is defined as `func (o Object) Read...`. It is **not** constructed with a pointer receiver (i.e. no `(o *Object)`). This is to make `Object` fit the `Readable` interface
* The SQL table queried from is the same as the object name. You can replace this with `reflect` to automate the name-getting
* You **MUST** add objects as pointers or else undesired behavior will enter your program and it will most likely not work
If you are creating a `Read` method with foreign relations, you need to perform more operations than a standard read.
For pointer relationships (**one-to-one** and **many-to-one**), you must get the referenced object by using the public method `GetObjectBySchema` and then cast it to the object you want. Here is an example from `examples/item-identification`:
```go
// Read reads in SQL values to the wrapper
func (r Identification) Read(db *sql.DB) (map[int]sql_wrapper.Readable, error) {
items := map[int]sql_wrapper.Readable{}
// Get the main elements
rows, err := db.Query("SELECT * FROM Identification")
if err != nil {
return items, err
}
defer rows.Close()
// Read for each row
var (
id int
number int
hash string
itemID int
)
for rows.Next() {
if err := rows.Scan(&id, &number, &hash, &itemID); err != nil {
return items, err
}
// Get the referenced item. We use a public method together with the name of the database/wrapper we want to find the link to. This gives us an object of the Readable interface
readable, err := sql_wrapper.GetObjectBySchema("Item", itemID)
if err != nil {
return items, err
}
// Now we can cast our readable object to the pointer we want
// NOTE: this cast will fail if you didn't save your object as a pointer!
item, ok := readable.(*Item)
if !ok {
return items, fmt.Errorf("cannot cast object to *post")
}
// Create and add the object
// NOTE: save the object as a pointer!
obj := Identification{Number: number, Hash: hash, Item: item}
items[id] = &obj
}
return items, nil
}
```
For slice relationships (**one-to-many** and **many-to-many**), you must get the rows of another SQL table that links the two wrappers together. This table is defined as the source struct's name concatenated with the target's struct name. Here is an example from `examples/user-post`:
```go
// Read reads in SQL values to the wrapper
func (r User) Read(db *sql.DB) (map[int]sql_wrapper.Readable, error) {
items := map[int]sql_wrapper.Readable{}
// Get the main elements
rows, err := db.Query("SELECT * FROM User")
if err != nil {
return items, err
}
defer rows.Close()
// Read for each row
var (
id int
name string
)
for rows.Next() {
if err := rows.Scan(&id, &name); err != nil {
return items, err
}
// Add the object normally
obj := User{Name: name}
items[id] = &obj
}
// Query the related elements
// NOTE: the source, or the struct we're writing Read for, is User. The target, or the struct being referenced by the source, is Post. So, the table name is UserPost, and ID columns follow the same order
rows, err = db.Query("SELECT * FROM UserPost")
if err != nil {
return items, err
}
defer rows.Close()
// Read for each row
var (
userID int
postID int
)
for rows.Next() {
// Scan in row elements
if err := rows.Scan(&userID, &postID); err != nil {
return items, err
}
// Get the reference object from the other wrapper
readable, err := sql_wrapper.GetObjectBySchema("Post", postID)
if err != nil {
return items, err
}
// Cast it to be the object we want
obj, ok := readable.(*Post)
if !ok {
return items, fmt.Errorf("cannot cast object to *Post")
}
// Get the user we want to link this item to
readable, ok = items[userID]
if !ok {
return items, fmt.Errorf("object was not saved from main table correctly (was it saved as a pointer?)")
}
user := readable.(*User)
// Add the target object to the corresponding source object
user.Posts = append(user.Posts, obj)
}
return items, nil
}
```
#### Creating New Wrappers
Now that your struct has been finished, a wrapper must be created to keep your struct objects synced to SQL. Creating a wrapper can be done as so with the required parameters.
```go
wrapper, err := sql_wrapper.NewWrapper[*Record](db, Record{})
```
In order to create a schema, you must provide:
* The SQL database needed to execute statements
* An empty struct you are parametrizing the schema with
* A generic type as a pointer, which lets the wrapper know what type to return
You can read in existing SQL entries using the `Read` function:
```go
if err := wrapper.Read(); err != nil {
// Handle error
}
```
You should call `Read` on wrappers without foreign references **first**. This allows other wrappers with foreign references to pull in relations after the other wrapper has loaded first.
#### Wrapper Functions
After your wrapper is created, you can then call functions associated with it. These are present in the examples and the [documentation][documentation-url].
### Examples
There are numerous examples present in the `examples` directory. You can check these out for help with your own project. You can do this by editing the SQL config struct present in the examples with your own values initialized on your own machine.
## Roadmap
- [x] Foreign Key Constraints
- [x] Find by ID Functions
- [ ] Foreign Relations with Maps
- [ ] Create GitHub Actions Workflow
See the [open issues][issues-url] for a full list of proposed features (and known issues).
## Contributing
For issues and suggestions, please include as much useful information as possible.
Review the [documentation][documentation-url] and make sure the issue is actually
present or the suggestion is not included. Please share issues/suggestions on the
[issue tracker][issues-url].
For patches and feature additions, please submit them as [pull requests][pulls-url].
Please adhere to the [conventional commits][conventional-commits-url]. standard for
commit messaging. In addition, please try to name your git branch according to your
new patch. [These standards][conventional-branches-url] are a great guide you can follow.
You can follow these steps below to create a pull request:
1. Fork the Project
2. Create your Feature Branch (`git checkout -b branch_name`)
3. Commit your Changes (`git commit -m "commit_message"`)
4. Push to the Branch (`git push origin branch_name`)
5. Open a Pull Request
## License
This project uses the Apache 2.0 License.
You can find more information in the [LICENSE][license-url] file.
## Contact
Ethan Baker - contact@ethanbaker.dev - [LinkedIn][linkedin-url]
Project Link: [https://github.com/ethanbaker/sql-wrapper][project-url]
[contributors-shield]: https://img.shields.io/github/contributors/ethanbaker/sql-wrapper.svg
[forks-shield]: https://img.shields.io/github/forks/ethanbaker/sql-wrapper.svg
[stars-shield]: https://img.shields.io/github/stars/ethanbaker/sql-wrapper.svg
[issues-shield]: https://img.shields.io/github/issues/ethanbaker/sql-wrapper.svg
[license-shield]: https://img.shields.io/github/license/ethanbaker/sql-wrapper.svg
[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?logo=linkedin&colorB=555
[contributors-url]:
[forks-url]:
[stars-url]:
[issues-url]:
[pulls-url]:
[license-url]:
[linkedin-url]:
[project-url]:
[documentation-url]:
[conventional-commits-url]:
[conventional-branches-url]: