Add 'Scan to arbitrary destination' wiki page.
This commit is contained in:
parent
1ab3ee4be6
commit
fb5bf7dd40
5 changed files with 538 additions and 8 deletions
|
|
@ -2,16 +2,16 @@
|
|||
## SQL Builder
|
||||
|
||||
SQL Builder files are Go files, containing types necessary to write type safe SQL queries in Go. They are
|
||||
autogenerated from database tables and enums.
|
||||
autogenerated from database tables and enums. File names are snake case of the table name or enum name.
|
||||
|
||||
### Table SQL Builder files
|
||||
|
||||
Following rules are applied to generate table SQL Builder files:
|
||||
|
||||
- for every table there is one Go SQL Builder file generated. File name is in snake case of the table name.
|
||||
- every file contains one type - struct with nested jet.Table. Type name is a camel case of table name.
|
||||
- for every column of table there is a field column in SQL Builder table type. Field name is camel case of column name.
|
||||
See below table for type mapping.
|
||||
- for every table there is one Go SQL Builder file generated.
|
||||
- every file contains one type - struct with nested jet.Table.
|
||||
- for every column of table there is a field column in SQL Builder table type.
|
||||
Field name is camel case of column name. See below table for type mapping.
|
||||
- `AllColumns` is used as shorthand notation for list of all columns.
|
||||
- `MutableColumns` are all columns minus primary key columns _(Useful in INSERT or UPDATE statements)_.
|
||||
|
||||
|
|
@ -82,7 +82,7 @@ type AddressTable struct {
|
|||
|
||||
Following rules are applied to generate enum SQL Builder files:
|
||||
|
||||
- for every enum there is one Go SQL Builder file generated. File name is in snake case of the enum name.
|
||||
- for every enum there is one Go SQL Builder file generated.
|
||||
- every file contains one type. Type name is a camel case of enum name.
|
||||
- for every enum value there is a field in SQL Builder enum struct. Field name is camel case of enum value. Type is
|
||||
jet.StringExpression, meaning it can be used by string expressions methods.
|
||||
|
|
|
|||
349
wiki/Scan-to-arbitrary-destination.md
Normal file
349
wiki/Scan-to-arbitrary-destination.md
Normal file
|
|
@ -0,0 +1,349 @@
|
|||
## Scan to arbitrary destination
|
||||
|
||||
Statements `Query` and `QueryContext` methods perform scan and grouping of row result to arbitrary `destination` structure.
|
||||
|
||||
- `Query(db execution.DB, destination interface{}) error` - executes statements over database connection db and stores row result in destination.
|
||||
- `QueryContext(db execution.DB, context context.Context, destination interface{}) error` - executes statement with a context over database connection db and stores row result in destination.
|
||||
|
||||
|
||||
### How scan works?
|
||||
|
||||
The easiest way to understand how scan works is by an example.
|
||||
|
||||
Lets say we want to retrieve list of cities, with list of customers for each city, and address for each customer.
|
||||
For simplicity we will narrow the choice to 'London' and 'York'.
|
||||
|
||||
Go SQL builder select statement:
|
||||
```
|
||||
stmt := City.
|
||||
INNER_JOIN(Address, Address.CityID.EQ(City.CityID)).
|
||||
INNER_JOIN(Customer, Customer.AddressID.EQ(Address.AddressID)).
|
||||
SELECT(
|
||||
City.CityID,
|
||||
City.City,
|
||||
Address.AddressID,
|
||||
Address.Address,
|
||||
Customer.CustomerID,
|
||||
Customer.LastName,
|
||||
).
|
||||
WHERE(City.City.EQ(String("London")).OR(City.City.EQ(String("York")))).
|
||||
ORDER_BY(City.CityID, Address.AddressID, Customer.CustomerID)
|
||||
```
|
||||
_Note that we are using jet select statement format([TODO]())_
|
||||
|
||||
Debug sql of above statement:
|
||||
```
|
||||
SELECT city.city_id AS "city.city_id",
|
||||
city.city AS "city.city",
|
||||
address.address_id AS "address.address_id",
|
||||
address.address AS "address.address",
|
||||
customer.customer_id AS "customer.customer_id",
|
||||
customer.last_name AS "customer.last_name"
|
||||
FROM dvds.city
|
||||
INNER JOIN dvds.address ON (address.city_id = city.city_id)
|
||||
INNER JOIN dvds.customer ON (customer.address_id = address.address_id)
|
||||
WHERE (city.city = 'London') OR (city.city = 'York')
|
||||
ORDER BY city.city_id, address.address_id, customer.customer_id;
|
||||
```
|
||||
|
||||
Every column is aliased by default. Format is "`table_name`.`column_name`"
|
||||
|
||||
Above statement will produce following result set:
|
||||
|
||||
|_row_| city.city_id | city.city | address.address_id | address.address | customer.customer_id | customer.last_name |
|
||||
|---| ------------ | ------------- | ------------------- | -------------------- | -------------------- | ------------------ |
|
||||
| _1_| 312 | "London" | 256 | "1497 Yuzhou Drive" | 252 | "Hoffman"|
|
||||
| _2_| 312 | "London" | 517 | "548 Uruapan Street"| 512 | "Vines" |
|
||||
| _3_| 589 | "York" | 502 | "1515 Korla Way" | 497 | "Sledge" |
|
||||
|
||||
Lets execute statement and scan result set to destination `dest`:
|
||||
```
|
||||
var dest []struct {
|
||||
model.City
|
||||
|
||||
Customers []struct{
|
||||
model.Customer
|
||||
|
||||
Address model.Address
|
||||
}
|
||||
}
|
||||
|
||||
err := stmt.Query(db, &dest)
|
||||
```
|
||||
|
||||
`Query` uses reflection to introspect destination type structure, and result set column names(aliases), to be able to map result set data to destination object.
|
||||
Note that camel case of result set column names(aliases) is the same as `model type name`.`field name`.
|
||||
For instance `city.city_id` -> `City.CityID`. This is being used to find appropriate column for each destination model field.
|
||||
It is not an error if there is not a column for each destination model field.
|
||||
|
||||
Lets see in general how `Query` works row by row:
|
||||
|
||||
- ROW 1:
|
||||
- dest is slice of structs, so new struct object is initialized and scan proceeds to next step.
|
||||
- `city.city_id` and `city.city` columns (values `312` and `"London"`) are used to initialize `CityID` and `City` fields of `model.City` object.
|
||||
- `Customers` is a slice of structs, so new struct object is initialized and scan proceeds to next step.
|
||||
- `customer.customer_id` and `customer.last_name` is used to initialize fields in `model.Customer` object.
|
||||
- `address.address_id` and `address.address` is used to initialize fields in `Address model.Address`
|
||||
- because at least one field of struct is being initialized struct is added to `Customers []struct` and cached by parent and
|
||||
struct primary key fields([more about primary key fields](TODO)). Primary keys used for caching are `CityID`, `CustomerID` and `AddressID` of `model.City`, `model.Customer`
|
||||
and `model.Address`
|
||||
- because at least one field of struct is being initialized struct is added to `var dest []struct` and cached by
|
||||
struct primary key fields. Primary keys used for caching is only `CityID` from `model.City`
|
||||
- ROW 2:
|
||||
- Does not initialize new struct object for `dest []struct` but pulls one from the cache, because `city` with `city_id` of `312` has
|
||||
already being processed. Following steps are the same as above, new objects are created, stored in slice and cached.
|
||||
- ROW 3:
|
||||
- steps would be similar as for the first step. Nothing is pulled from he cache, stored in slice and cached.
|
||||
|
||||
|
||||
Lets print `dest` as a json, to visualize `Query` result:
|
||||
|
||||
```
|
||||
[
|
||||
{
|
||||
"CityID": 312,
|
||||
"City": "London",
|
||||
"CountryID": 0,
|
||||
"LastUpdate": "0001-01-01T00:00:00Z",
|
||||
"Customers": [
|
||||
{
|
||||
"CustomerID": 252,
|
||||
"StoreID": 0,
|
||||
"FirstName": "",
|
||||
"LastName": "Hoffman",
|
||||
"Email": null,
|
||||
"AddressID": 0,
|
||||
"Activebool": false,
|
||||
"CreateDate": "0001-01-01T00:00:00Z",
|
||||
"LastUpdate": null,
|
||||
"Active": null,
|
||||
"Address": {
|
||||
"AddressID": 256,
|
||||
"Address": "1497 Yuzhou Drive",
|
||||
"Address2": null,
|
||||
"District": "",
|
||||
"CityID": 0,
|
||||
"PostalCode": null,
|
||||
"Phone": "",
|
||||
"LastUpdate": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"CustomerID": 512,
|
||||
"StoreID": 0,
|
||||
"FirstName": "",
|
||||
"LastName": "Vines",
|
||||
"Email": null,
|
||||
"AddressID": 0,
|
||||
"Activebool": false,
|
||||
"CreateDate": "0001-01-01T00:00:00Z",
|
||||
"LastUpdate": null,
|
||||
"Active": null,
|
||||
"Address": {
|
||||
"AddressID": 517,
|
||||
"Address": "548 Uruapan Street",
|
||||
"Address2": null,
|
||||
"District": "",
|
||||
"CityID": 0,
|
||||
"PostalCode": null,
|
||||
"Phone": "",
|
||||
"LastUpdate": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"CityID": 589,
|
||||
"City": "York",
|
||||
"CountryID": 0,
|
||||
"LastUpdate": "0001-01-01T00:00:00Z",
|
||||
"Customers": [
|
||||
{
|
||||
"CustomerID": 497,
|
||||
"StoreID": 0,
|
||||
"FirstName": "",
|
||||
"LastName": "Sledge",
|
||||
"Email": null,
|
||||
"AddressID": 0,
|
||||
"Activebool": false,
|
||||
"CreateDate": "0001-01-01T00:00:00Z",
|
||||
"LastUpdate": null,
|
||||
"Active": null,
|
||||
"Address": {
|
||||
"AddressID": 502,
|
||||
"Address": "1515 Korla Way",
|
||||
"Address2": null,
|
||||
"District": "",
|
||||
"CityID": 0,
|
||||
"PostalCode": null,
|
||||
"Phone": "",
|
||||
"LastUpdate": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
All the fields missing source column in result set are initialized with empty value.
|
||||
City of `London` has two customers, which is the product of object reuse in `ROW 2` processing.
|
||||
|
||||
### Custom model files
|
||||
|
||||
**Programmes are not limited to just model files, any destination will work, as long as camel case of result set column
|
||||
is equal to `model type name`.`field name`.**
|
||||
|
||||
#### Named types
|
||||
|
||||
Lets rewrite above example to use custom named model files:
|
||||
|
||||
```
|
||||
type MyAddress struct {
|
||||
ID int32 `sql:"primary_key"`
|
||||
AddressLine string
|
||||
}
|
||||
|
||||
type MyCustomer struct {
|
||||
ID int32 `sql:"primary_key"`
|
||||
LastName *string
|
||||
|
||||
Address MyAddress
|
||||
}
|
||||
|
||||
type MyCity struct {
|
||||
ID int32 `sql:"primary_key"`
|
||||
Name string
|
||||
|
||||
Customers []MyCustomer
|
||||
}
|
||||
|
||||
dest2 := []MyCity{}
|
||||
|
||||
stmt2 := City.
|
||||
INNER_JOIN(Address, Address.CityID.EQ(City.CityID)).
|
||||
INNER_JOIN(Customer, Customer.AddressID.EQ(Address.AddressID)).
|
||||
SELECT(
|
||||
City.CityID.AS("my_city.id"), //snake case
|
||||
City.City.AS("myCity.Name"), //camel case
|
||||
Address.AddressID.AS("My_Address.id"), //mixed case
|
||||
Address.Address.AS("my address.address line"), //with spaces
|
||||
Customer.CustomerID.AS("my_customer.id"),
|
||||
Customer.LastName.AS("my_customer.last_name"),
|
||||
).
|
||||
WHERE(City.City.EQ(String("London")).OR(City.City.EQ(String("York")))).
|
||||
ORDER_BY(City.CityID, Address.AddressID, Customer.CustomerID)
|
||||
|
||||
err := stmt2.Query(db, &dest2)
|
||||
```
|
||||
|
||||
Destination type names and field names are now changed. Every type has 'My' prefix, every primary key column is named `ID`,
|
||||
`FirstName` is now string pointer etc.
|
||||
Because we are using custom types with changed identifier, every column has to be aliased.
|
||||
For instance: `City.CityID.AS("my_city.id")`, ` City.City.AS("myCity.Name")` etc.
|
||||
**Table names, column names and aliases doesn't have to be in a snake case. CamelCase, PascalCase or some other mixed space is also supported,
|
||||
but it is strongly recommended to use snake case for database identifiers.**
|
||||
|
||||
Json of new destination is also changed:
|
||||
|
||||
```
|
||||
[
|
||||
{
|
||||
"ID": 312,
|
||||
"Name": "London",
|
||||
"Customers": [
|
||||
{
|
||||
"ID": 252,
|
||||
"LastName": "Hoffman",
|
||||
"Address": {
|
||||
"ID": 256,
|
||||
"AddressLine": "1497 Yuzhou Drive"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": 512,
|
||||
"LastName": "Vines",
|
||||
"Address": {
|
||||
"ID": 517,
|
||||
"AddressLine": "548 Uruapan Street"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ID": 589,
|
||||
"Name": "York",
|
||||
"Customers": [
|
||||
{
|
||||
"ID": 497,
|
||||
"LastName": "Sledge",
|
||||
"Address": {
|
||||
"ID": 502,
|
||||
"AddressLine": "1515 Korla Way"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### Antonymous types
|
||||
|
||||
There is no need to create new named type for every custom model.
|
||||
Destination type can be declared inline without naming any type.
|
||||
|
||||
```
|
||||
var dest []struct {
|
||||
CityID int32 `sql:"primary_key"`
|
||||
CityName string
|
||||
|
||||
Customers []struct {
|
||||
CustomerID int32 `sql:"primary_key"`
|
||||
LastName string
|
||||
|
||||
Address struct {
|
||||
AddressID int32 `sql:"primary_key"`
|
||||
AddressLine string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stmt := City.
|
||||
INNER_JOIN(Address, Address.CityID.EQ(City.CityID)).
|
||||
INNER_JOIN(Customer, Customer.AddressID.EQ(Address.AddressID)).
|
||||
SELECT(
|
||||
City.CityID.AS("city_id"),
|
||||
City.City.AS("city_name"),
|
||||
Customer.CustomerID.AS("customer_id"),
|
||||
Customer.LastName.AS("last_name"),
|
||||
Address.AddressID.AS("address_id"),
|
||||
Address.Address.AS("address_line"),
|
||||
).
|
||||
WHERE(City.City.EQ(String("London")).OR(City.City.EQ(String("York")))).
|
||||
ORDER_BY(City.CityID, Address.AddressID, Customer.CustomerID)
|
||||
|
||||
err := stmt.Query(db, &dest)
|
||||
```
|
||||
Aliasing is now simplified. Alias contains only (column/field) name.
|
||||
On the other hand, we can not have 3 fields named `ID`, because aliases have to be unique.
|
||||
|
||||
|
||||
### Combining autogenerated and custom model files
|
||||
|
||||
It is allowed to combine autogenerated and custom model files.
|
||||
For instance:
|
||||
|
||||
```
|
||||
type MyCustomer struct {
|
||||
ID int32 `sql:"primary_key"`
|
||||
LastName string
|
||||
|
||||
Address model.Address //model.Address is autogenerated model type
|
||||
}
|
||||
|
||||
type MyCity struct {
|
||||
ID int32 `sql:"primary_key"`
|
||||
Name string
|
||||
|
||||
Customers []MyCustomer
|
||||
}
|
||||
```
|
||||
|
|
@ -9,15 +9,23 @@ Following statements are supported:
|
|||
|
||||
_This list might be extended with feature Jet releases._
|
||||
|
||||
There is a common set of action that can be performed for each statement type:
|
||||
Statements SQL can be debugged in two ways:
|
||||
|
||||
- `Sql() (query string, args []interface{}, err error)` - retrieves parametrized sql query with list of arguments
|
||||
- `DebugSql() (query string, err error)` - retrieves debug query where every parametrized placeholder is replaced with its argument.
|
||||
|
||||
Statements can be executed by following methods:
|
||||
- `Query(db execution.DB, destination interface{}) error` - executes statements over database connection db and stores row result in destination.
|
||||
- `QueryContext(db execution.DB, context context.Context, destination interface{}) error` - executes statement with a context over database connection db and stores row result in destination.
|
||||
- `Exec(db execution.DB) (sql.Result, error)` - executes statement over db connection without returning any rows.
|
||||
- `ExecContext(db execution.DB, context context.Context) (sql.Result, error)` - executes statement with context over db connection without returning any rows.
|
||||
|
||||
Each execution method first creates parametrized sql query with list of arguments and then initiates query on database connection.
|
||||
Exec and ExecContext are just a wrappers around database `Exec` and `ExecContext`.
|
||||
|
||||
`Query` and `QueryContext` are bit more complex, the are wrappers around database `Query` and `QueryContext`,
|
||||
but they also perform grouping of row result to arbitrary `destination` structure.
|
||||
|
||||
Database connection can be of any type that implements following interface:
|
||||
|
||||
```go
|
||||
|
|
|
|||
|
|
@ -10,4 +10,4 @@
|
|||
* [UPDATE](https://github.com/go-jet/jet/wiki/UPDATE)
|
||||
* [DELETE](https://github.com/go-jet/jet/wiki/DELETE)
|
||||
* [LOCK](https://github.com/go-jet/jet/wiki/LOCK)
|
||||
* [Execution](https://github.com/go-jet/jet/wiki/Execution)
|
||||
* [Scan to arbitrary destination](https://github.com/go-jet/jet/wiki/Scan-to-arbitrary-destination.md)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue