You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

270 lines
10 KiB
Go

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

package main
import (
"database/sql"
"flag"
"log"
_ "github.com/mattn/go-sqlite3"
)
func showTransactionStmtClose(p *sql.Stmt) {
log.Println("Closing statement!")
p.Close()
}
func main() {
log.Println("Hello, world!")
db_path := flag.String("database", "./data/trysqlite.db", "Path to a Sqlite3 database")
flag.Parse()
// TODO figure out what query string options to be using
full_database_path := "file:" + *db_path + "?cache=shared"
log.Printf("Using Database: %s", full_database_path)
// db is a Handle backed by a go database/sql connection pool. It is not a connection in of itself, nor is it the actual connection pool
// The first connection is made lazily by the handle, and this doesn't validate any of the connection parameters either at this point.
// It simply prepares the abstraction for use.
db, err := sql.Open("sqlite3", full_database_path)
if err != nil {
log.Fatalf("Failed to connect to sqlite database: %s", full_database_path)
}
// It is idiomatic to defer db.Close() if the sql.DB should not have a lifetime beyond the scope of the function.
//Although its idiomatic to Close() the database when youre finished with it, the sql.DB object is designed to be long-lived.
//Dont Open() and Close() databases frequently. Instead, create one sql.DB object for each distinct datastore you need to access,
// and keep it until the program is done accessing that datastore.
// Pass it around as needed, or make it available somehow globally, but keep it open.
// And dont Open() and Close() from a short-lived function. Instead, pass the sql.DB
// into that short-lived function as an argument.
//If you dont treat the sql.DB as a long-lived object, you could experience problems such as poor reuse and sharing of connections,
// running out of available network resources, or sporadic failures due to a lot of TCP connections remaining in TIME_WAIT status.
// Such problems are signs that youre not using database/sql as it was designed.
defer db.Close()
// If you want to check right away that the db is available and accessible you can do this and check for errs
err = db.Ping()
if err != nil {
log.Fatal("DB Ping failed. Check database and database connection parameters")
}
var (
id int
subject string
todo string
)
// db.Query vs db.Exec. If a function name includes Query, it is designed to ask a question of the database,
// and will return a set of rows, even if its empty. Statements that dont return rows should not use Query functions;
// they should use Exec()
rows, err := db.Query("SELECT subject FROM todos")
if err != nil {
log.Fatal(err)
}
// It is important to defer closing a row object also. Why? It will release the memory and the network connection. So it
// also prevents resource leaks. So you are a good steward, you clean up after yourself.
// You don't wait for the garbage collector to do it.
// as long as theres an open result set (represented by rows), the underlying connection is busy and cant be used for
//any other query. It's busy because sql.Rows objects don't retrieve all results a query returns. It does it lazily to prevent over runing memory.
// it depends on the driver but there is usually and internal buffer that stores results.
// Also never defer in a loop. Defer only runs on function exit.
defer rows.Close()
// Iterate through each. Internally rows.Next will hit an EOF error it will call rows.close() for you freeing up the connection resources.
// but you shouldn't rely on that alone. rows.Close() will run on function exit, but this example is a Main func, so it's not realistic
// still get in the habit
for rows.Next() {
err = rows.Scan(&subject)
if err != nil {
log.Fatal(err)
}
log.Printf("Subject is %s", subject)
}
// Dont just assume that the loop iterates until youve processed all the rows. Always check.
err = rows.Err()
if err != nil {
log.Fatal(err)
}
// You could also clean up at this point. The call is supposed to be idempotent, but defer is more idiomatic.
// You don't want to call close if there was an Error because of that could cause a Panic.
rows.Close()
// If never want to do string concat from user input into a sql statement you run unless you like sql injection attacks
// expects to return one row. If no row it will delay throwing error until Scan is called
row := db.QueryRow("SELECT id, subject, todo FROM todos WHERE id = ?", 1)
// Scan does the heavily lifting for you when casting from db type to go type. It will cast based on the variable type.
// failures of course will be returned as part of the err
err = row.Scan(&id, &subject, &todo)
if err != nil {
log.Fatal(err)
}
log.Printf("id: %d, subject: %s, todo: %s", id, subject, todo)
// This should error on scan
// We can also use this short hand!
err = db.QueryRow("SELECT * FROM todos WHERE id = ?", 100_000).Scan(&id, &subject, &todo)
if err != nil {
log.Print(err.Error())
}
// There is no row.Close(). Rows objects have a Close() method. No explicit closing required.
//Prepared Queries
// If you will be using the same query over and over you should just prepare the query.
// $N (e.g. $1, $2, etc...) is the postgres query param which SQLite can use too ? is mysql's
//Under the hood, db.Query() actually prepares, executes, and closes a prepared statement
// Thats three round-trips to the database.
stmt, err := db.Prepare("SELECT todo as other_todo FROM todos WHERE id = $1")
if err != nil {
log.Fatal(err)
}
//This prepared statement *sql.Stmt is tied to the database connection from which it was created, and it holds onto that connection until you call stmt.Close()
defer stmt.Close()
// You can also call with just params the .QueryRow method since the statement is already prepared
var other_todo string
err = stmt.QueryRow(1).Scan(&other_todo)
if err != nil {
log.Fatal(err)
}
log.Printf("Other Todo: %s", other_todo)
stmt.Close()
// MODIFYING DATA
// Use db.Exec() with a prepared statement when INSERTING, UPDATING, or DELETING
// Or another statement that doesn't return rows.
stmt, err = db.Prepare("INSERT INTO todos (subject, todo) VALUES ($1, $2)")
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
// stmt.Exec returns a sql.Result. That also has access to the statement metadata
// use _ if you don't care about the result and only want to check the err.
// Exec will NOT reserve a connection. unlike db.Query which returns a sql.Rows that
// will hold on to a connection until .Close() is called.
res, err := stmt.Exec("programming", "learn golang database queries!")
if err != nil {
log.Print("Dirp...")
log.Fatal(err)
}
lastId, err := res.LastInsertId()
if err != nil {
log.Fatal(err)
}
log.Printf("Last inserted id: %d", lastId)
rowCnt, err := res.RowsAffected()
if err != nil {
log.Fatal(err)
}
log.Printf("The number of rows affected: %d", rowCnt)
// close insert
stmt.Close()
stmt, err = db.Prepare("DELETE FROM todos WHERE subject = 'programming'")
if err != nil {
log.Fatal(err)
}
res, err = stmt.Exec()
if err != nil {
log.Fatal(err)
}
rowCnt, err = res.RowsAffected()
if err != nil {
log.Fatal(err)
}
log.Printf("The number of rows affected: %d", rowCnt)
// TRANSACTIONS
//In Go, a transaction is essentially an object that reserves a connection to the datastore.
//It lets you do all of the operations weve seen thus far, but guarantees that theyll be
//executed on the same connection.
// Starts with db.Begin() returning a sql.Tx and ends with a .Commit() or .Rollback() on the results sql.Tx
tx, err := db.Begin()
if err != nil {
log.Fatal(err)
}
// Prepared statement in a transaction are bound to that transaction exclusively.
// DO NOT use sql BEGIN or COMMIT in a sql.Tx object. It could leave the DB and client in different states.
// or mess havoc with the connection pool and connection lifetime
insert_stmt, err := tx.Prepare("INSERT INTO todos (subject, todo) VALUES ($1, $2)")
if err != nil {
log.Fatal(err)
}
defer insert_stmt.Close() // This closes at the end of the transaction, not the end of the function.
_, _ = insert_stmt.Exec("dirp", "just a place holder")
// There is no Close method on a transaction. It is closed by using .Commit or .Rollback
tx.Rollback()
// PREPARED STATEMENTS IN MORE DEPTH
//At the database level, a prepared statement is bound to a single database connection.
//The typical flow is that the client sends a SQL statement with placeholders to the server
//for preparation, the server responds with a statement ID, and then the client executes
//the statement by sending its ID and parameters.
// THE ABOVE DOES NOT HAPPEN IN database/sql. Instead the statement is managed at the driver level
// Go doesn't expose connections directly to users. The sql.Stmt object remembers which connection
// from the pool it is associated with.
//Know that if a connection is busy it may automatically get a new connection and re-prepare the
// statement on a new connection. It could cause more connections than you think, more statement
// recreation, or even hitting a server limit on prepared statements.
// NOTE db.Query(sql, param1, param2) will create a prepared statement under the hood.
// If you dont want to use a prepared statement, you need to use fmt.Sprint() or similar to
//assemble the SQL, and pass this as the only argument to db.Query() or db.QueryRow() And your driver
//needs to support plaintext query execution,
// NOTE prepared statements on Tx are bound exclusively to the Tx. You cannot use a prepared statement
// created by db.Prepare nor can you use a Tx.Prepare statement with a db.Query. Tx.Stmt() can be used
// to copy a db.Prepared statement into a transaction, but don't do this.
// HANDLING ERRORS
// You should always explicitly close a sql.Rows result set. If rows.Close() returns an error, its
// unclear what you should do, so logging the error message and ignoring or panic-ing might be the only sensible
// thing to do.
// Use sql.ErrNoRows in the event that no data is returned.
// rememeber db.QueryRow defers errors till Scan is called.
err = db.QueryRow("select subject from todos where id = $1", 100_000).Scan(&subject)
if err != nil {
if err == sql.ErrNoRows {
// there were no rows, but otherwise no error occurred
log.Println("No rows were returned! But that is okay. We expected that could happen")
} else {
log.Fatal(err)
}
}
}