Firebase 學習記錄

因為網路上的教學有些有點舊,所以整理了一篇 firebase 學習記錄。

  1. 建立專案,參考這篇
  2. 寫入資料(不覆蓋路徑下舊有的資料,但為了區別所以會自動產生一個亂數的key)
     function write(groupid, userid, username, message) {
       push(ref(db, '/group/'+ groupid), {
         userid: userid,
         username: username,
         message: message,
         timestamp: Date.now()
       })
     }
    
  3. 讀取資料,存入 itemList 後回傳
     function read(groupid) {
       const itemList = []
          
       onValue(ref(db, '/group/'+groupid), (snapshot) => {
         snapshot.forEach(function (snapshot) {
           var obj = snapshot.val();
           itemList.push(obj)
       })
       }, {
         onlyOnce: true
       });
    
       return itemList
     }
    
  4. 以覆蓋方式寫入資料(重置資料庫)
     function resetDB() {
       set(ref(db, '/group'), {});
     }
    

完整程式碼:

import { initializeApp } from "firebase/app";
import { getDatabase, ref, set, push, onValue} from "firebase/database";

const firebaseConfig = {
  apiKey: "你的 api key",
  authDomain: "你的 authDomain",
  databaseURL: "你的 db URL",
  projectId: "你的 projectId",
  storageBucket: "你的 storageBucket",
  messagingSenderId: "你的 messagingSenderId",
  appId: "你的 appId",
  measurementId: "你的 measurementId"
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);
const db = getDatabase(app);

export function resetDB() {
  set(ref(db, '/group'), {});
}
export function write(groupid, userid, username, message) {
  push(ref(db, '/group/'+ groupid), {
    userid: userid,
    username: username,
    message: message,
    timestamp: Date.now()
  })
}

export function read(groupid) {
  const itemList = []
  
  onValue(ref(db, '/group/'+groupid), (snapshot) => {
    snapshot.forEach(function (snapshot) {
      var obj = snapshot.val();
      itemList.push(obj)
  })
  }, {
    onlyOnce: true
  });

  return itemList
}

Reference

Firebase doc

MySQL - invalid memory address or nil pointer dereference

在 Go 連線資料庫之後做操作,卻出現以下錯誤:

runtime error: invalid memory address or nil pointer dereference

後來發現是 db 在程式中有重複宣告,所以以後出現這類型錯誤,先檢查物件是否是 nil。

Netlify & React - Page not found

在 Netlify 部署好 React 網站後,卻發現某些頁面點擊會顯示 Page Not Found. Looks like you’ve followed a broken link 這樣的錯誤。

這是因為 Netlify 不知道 root route 之外的 route 要怎麼去 locate。以下是我的解決方式:

  1. public 資料夾底下建立檔案 _redirects
  2. 在這個檔案中貼上內容: /* /index.html 200
  3. 存擋,重新 deploy & publish !

Reference

Netlify React Router Not Working: 5 Simple Steps to Fix it

MySQL - You have an error in your SQL syntax

在執行 source xx.sql 執行 SQL 指令的時候,一直出現以下錯誤:

ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '一些SQL指令'

後來發現是 CREATE 指令結束、 INSERT 指令前沒有加上分號……特地打一篇警惕自己!

Go - gin 學習記錄

上次學了怎麼用 Go 和 SQL 交互,接著要利用 gin package 來寫 api!下面範例沒有用到資料庫,只是先用陣列存一些資料做存取與新增。真正用到資料庫的程式碼在文章最下面,是延續使用Go - mysql 學習記錄這篇架的資料庫。

初始化專案

mkdir example && cd example
go mod init example

開始寫程式!取得資料的 handler!

  1. 建立 main.go
     touch main.go
    
  2. main.go 貼上:
     package main
    
     import (
         "net/http"
    
         "github.com/gin-gonic/gin"
     )
    
  3. 建立資料的結構,在 main.go 貼上下面程式碼
     // album 結構代表會與之後從資料庫取回的專輯資料互相 match
     type album struct {
         ID     string  `json:"id"`
         Title  string  `json:"title"`
         Artist string  `json:"artist"`
         Price  float64 `json:"price"`
     }
    
     // 資料
     var albums = []album{
         {ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99},
         {ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99},
         {ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99},
     }
    
  4. 貼上下面的 function,這是一個會取得 albums 訊息的 handler:
     func getAlbums(c *gin.Context) {
         c.IndentedJSON(http.StatusOK, albums)
     }
    
  5. 貼上下面程式碼,利用 gin 建立 Router,讓 getAlbums handler handle 到 /albums path 的 GET 請求:
     func main() {
         router := gin.Default()
         router.GET("/albums", getAlbums)
    
         router.Run("localhost:8080") // start the server
     }
    
  6. 在 terminal 執行 go mod tidygo get . 來取得需要的 dependency
  7. go run . 啟動剛剛寫好的 http server
  8. 在 terminal 用 curl 指令和 server 交互:
     curl http://localhost:8080/albums
    

添加資料的 handler!

  1. 把 POST method 的 request body 轉換成 album struct 後添加到 albums ,也就是我們的資料中:
    func postAlbums(c *gin.Context) {
        var newAlbum album
    
        // 把接收到的 json body 轉換成 struct
        if err := c.BindJSON(&newAlbum); err != nil {
            return
        }
    
        // 添加新專輯到 albums 陣列
        albums = append(albums, newAlbum)
        c.IndentedJSON(http.StatusCreated, newAlbum)
    }
    
  2. main.gorouter.GET("/albums", getAlbums) 下加上一行:
     router.POST("/albums", postAlbums)
    
  3. go run . 重新執行 server
  4. 測試看看吧!
     curl http://localhost:8080/albums \
     --include \
     --header "Content-Type: application/json" \
     --request "POST" \
     --data '{"id": "4","title": "The Modern Sound of Betty Carter","artist": "Betty Carter","price": 49.99}'
    
  5. 用前面的 GET method 確認資料真的被添加進去:
     curl http://localhost:8080/albums \
         --header "Content-Type: application/json" \
         --request "GET"
    

用 uri 指定取得特定資料!

  1. 貼上 handler function:
     func getAlbumByID(c *gin.Context) {
         id := c.Param("id") // 從 path 裡面取出 id parameter
    
         // 用迴圈來找這個 id 是對應哪張專輯
         for _, a := range albums {
             if a.ID == id {
                 c.IndentedJSON(http.StatusOK, a)
                 return
             }
         }
         c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
     }
    
  2. main.gorouter.GET("/albums", getAlbums) 下面貼上:
     router.GET("/albums/:id", getAlbumByID)
    
  3. 在 terminal 用 curl 打打看這個 api:
     curl http://localhost:8080/albums/2
    

把程式碼變成真的和資料庫交互!

之前建立的資料庫還在,裡面的資料也和前面的 album 一樣,如果 handler 與 path 都不變,將資料從陣列改成真正的資料庫資料的話,程式如下:

package main

import (
	"database/sql"
	"fmt"
	"log"
	"net/http"
	"os"
	"strconv"

	"github.com/go-sql-driver/mysql"

	"github.com/gin-gonic/gin"
)

var db *sql.DB

// album represents data about a record album.
type Album struct {
	ID     string  `json:"id"`
	Title  string  `json:"title"`
	Artist string  `json:"artist"`
	Price  float64 `json:"price"`
}

func main() {
	// db config
	cfg := mysql.Config{
		User:   os.Getenv("DBUSER"), //export DBUSER=你的 MySQL 用戶名 
		Passwd: os.Getenv("DBPASS"), //export DBPASS=你的 MySQL password 
		Net:    "tcp",
		Addr:   "127.0.0.1:3306",
		DBName: "recordings",
	}
	// 連接資料庫
	var err error
	db, err = sql.Open("mysql", cfg.FormatDSN())
	if err != nil {
		log.Fatal(err)
	}

	pingErr := db.Ping() // 確認是否真的連接上資料庫
	if pingErr != nil {
		log.Fatal(pingErr)
	}
	fmt.Println("Connected!")

  // router 的部分
	router := gin.Default()
	router.GET("/albums", getAlbums)
	router.POST("/albums", postAlbums)
	router.GET("/albums/:id", getAlbumByID)

	router.Run("localhost:8080")
}

// 取得所有專輯資料
func getAlbums(c *gin.Context) {
	var albums []Album
	rows, err := db.Query("SELECT * FROM album")
	if err != nil {
		c.IndentedJSON(http.StatusNotFound, gin.H{"message": "getAlbums error"})
	}
	defer rows.Close()

	for rows.Next() {
		var alb Album
		if err := rows.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price); err != nil {
			c.IndentedJSON(http.StatusNotFound, gin.H{"message": "getAlbums error"})
		}
		albums = append(albums, alb)
	}
	if err := rows.Err(); err != nil {
		c.IndentedJSON(http.StatusNotFound, gin.H{"message": "getAlbums error"})
	}

	c.IndentedJSON(http.StatusOK, albums)
}

// 新增專輯
func postAlbums(c *gin.Context) {
	var newAlbum Album

	// 把接收到的 json body 轉換成 struct
	if err := c.BindJSON(&newAlbum); err != nil {
		c.IndentedJSON(http.StatusNotFound, gin.H{"message": "getAlbums error"})
	}

	result, err := db.Exec("INSERT INTO album (title, artist, price) VALUES (?, ?, ?)", newAlbum.Title, newAlbum.Artist, newAlbum.Price)
	if err != nil {
		c.IndentedJSON(http.StatusNotFound, gin.H{"message": "getAlbums error"})
	}
	id, err := result.LastInsertId()
	if err != nil {
		c.IndentedJSON(http.StatusNotFound, gin.H{"message": "getAlbums error"})
	}

	c.IndentedJSON(http.StatusCreated, "新專輯的 id 是:"+strconv.Itoa(int(id)))
}

// 使用 id 取得特定專輯
func getAlbumByID(c *gin.Context) {
	id := c.Param("id")

	var alb Album
	row := db.QueryRow("SELECT * FROM album WHERE id = ?", id)
	if err := row.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price); err != nil {
		if err != nil {
			c.IndentedJSON(http.StatusNotFound, gin.H{"message": err})
		}
	}

	c.IndentedJSON(http.StatusOK, alb)
}

錯誤處理的部分是亂寫的,統一使用 http.StatusNotFound 敷衍過去,但實際上應該有優雅又精確的處理辦法,之後研究完再來更新這篇!

Reference

Tutorial: Developing a RESTful API with Go and Gin Tutorial: Accessing a relational database