2020-12-07

定期備份 mongodb 並上傳到 s3

定期備份 mongodb 並上傳到 s3

使用 mongodump 滙出資料庫,並以 tar 壓縮,再以 s3cmd 上傳至 AWS S3 備份。

寫成 backup-mongo.sh,搭配 crontab 就可以定時備份了!以下中文的部份請自行換成真實資料:

#!/bin/bash # 設定當日日期字串為變數值 today=`date +"%Y-%m-%d"` # 將資料庫倒至暫存資料夾 /usr/bin/mongodump -h 芒果主機 -u 芒果用戶 -p 用戶密碼 --authenticationDatabase admin --db my_database --out "/home/ubuntu/mongo_data/bk-${today}" # 壓縮資料夾 /usr/bin/tar -zcvf "/home/ubuntu/mongo_data/my_database-${today}.tar.gz" -C "/home/ubuntu/mongo_data/bk-${today}" my_database # 使用 s3cmd 上傳壓縮檔到 s3 /usr/bin/s3cmd put "/home/ubuntu/mongo_data/my_database-${today}.tar.gz" s3://my_database/mongo-backup/ # 移除暫存資料夾 /usr/bin/rm -fr "/home/ubuntu/mongo_data/bk-${today}" # 移除暫存壓縮檔 /usr/bin/rm "/home/ubuntu/mongo_data/my_database-${today}.tar.gz"

要注意的是若使用 root 執行,在 /root 目錄下要有 s3cmd 的設定檔 .s3cfg。

2020-12-06

EC2 Ubuntu 上無法使用 crontab -e

EC2 Ubuntu 上無法使用 crontab -e

在 EC2 上開的 ubuntu,遇到使用 crontab 一直沒有作用的情況。搞了老半天爬文才發現不少人遇到同樣的情況

如連結中的說明,直接使用 sudo nano 編輯 /etc/crontab 即可。

之前使用 NAS 也有遇過類似的情況,不過 NAS 本來就硬體資源有限,在 EC2 上遇到還滿驚訝的啊!

2020-11-27

Mailgun 資源

mailgun 資源

以 mailgun 使用程式發信時,首先要有自己的 domain,個人是使用 godaddy 的網域。 Mailgun 官網是以 Cloudflare 為範例說明,來說明如何設定網域的驗證:Domain-Verification-Walkthrough。 設定的 domain 以 mg.mydomain.com 為例。

申請 Sending API Keys

到 Mailgun 官網登入後,在左側欄點選「Sending」>「Domain」,選擇你要使用的 domain 後,選「Domain Settings」,再點選「Sending API Keys」建立所需的發信 key。

使用 Node.js 發信

官方有出 Node.js package mailgun-js,先在專案中安裝。 參考這篇官方的文件 How To Send Transactional Email In A NodeJS App Using The Mailgun API。 我的 .env 設定大概是長這樣:

MAILGUN_API_KEY=my_secret_key MAILGUN_FROM_WHO=service@mg.mydomain.com MAILGUN_DOMAIN=mg.mydomain.com

測試發給自己信箱時,有可能會進到垃圾郵件,如果沒看到郵件,先到垃圾郵件找找找不到的話,再進行除錯。

官方會提供一個 postmaster@mg.mydomain.com 的帳號,可以在 Domain settings > SMTP credentials 那邊看到。 重設密碼後,應該可以使用 nodemailer 發信。有些人以 nodemailer 搭配 nodemailer-mailgun-transport 使用。

2020-11-21

在 ubuntu 使用 screen 背景執行

在 ubuntu 使用 screen 背景執行

通常終端機指令在其後下 & 就可以背景執行,不過很不方便。screen 用起來就方便多了。 尤其是使用 ssh 遠端登入,就算網路斷線或者筆電進入睡眠狀態,也不用擔心程式跑一半就斷了;同時又可以接回去原本的訊息畫面。

# 查看有沒有安裝 screen $ screen --version # 若沒有安裝,以 apt 安裝 $ sudo apt install screen # 查看背景執行的程式 $ screen -ls # 開始,直接下 screen 指令,然後按 Enter 進入即可 $ screen # 進入後像是進入另一個 shell 一樣,執行你要執行的指令後,可以關掉 terminal # 退出 screen $ exit

以下是查看的例子:

$ screen -ls There are screens on: 220648.pts-4.backup-sfo2-20201024 (11/20/20 11:05:38) (Detached) 210531.pts-0.backup-sfo2-20201024 (11/18/20 23:00:07) (Attached)

使用參數 -r 可以連接目前分離狀態 (Detached) 的 terminal

# id 打前面幾個字元按 tab 即可 $ screen -r 220648.pts-4.backup-sfo2-20201024

使用參數 -x 可以搶用目前不是在分離狀態 (Attached) 的 terminal

# id 打前面幾個字元按 tab 即可 $ screen -x 210531.pts-0.backup-sfo2-20201024

2020-11-20

在 ubuntu 建立磁碟交換區 swap

在 ubuntu 建立磁碟交換區 swap (暫存區)

本篇筆記參考How To Add Swap Space on Ubuntu 16.04

文中警告,swap 可能會降低 SSD 硬碟的壽命!

# 查看是否有磁碟交換區 $ sudo swapon --show # 查看記憶體使用情況(亦會顯示交換區) $ free # 建立 swap 之前,先了解磁碟的使用狀況 $ df -h # 建立一個用來當做 swap 的檔案 swapfile(名稱可自己決定), # 大小為 4g,通常為實體記憶體的一半到相同大小,視情況而定 $ sudo fallocate -l 4G /swapfile # 變更為只有 root 能用 $ sudo chmod 600 /swapfile # 設定檔案為交換區 $ sudo mkswap /swapfile # 啟用檔案為交換區 $ sudo swapon /swapfile # 若要在下次重開機時,重新掛載 swap,需編輯 /etc/fstab # 先備份檔案,以免發生悲劇 $ sudo cp /etc/fstab /etc/fstab.bak # 將設定內容加入檔案,就全部完成 $ echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

2020-11-08

使用 docker-node 測試 nodejs 專案

使用 docker-node 測試 nodejs 專案

首先你的環境必須已經安裝 docker,參考「取得 docker」

# 取得 nodejs,在此使用較舊版本 10 docker pull node:10

在專案中建立 docker-compose.yml 檔:

version: "2" services: node: image: "node:10" working_dir: /Users/shinder/Dropbox/my-nodejs-proj environment: - NODE_ENV=development volumes: - ./:/Users/shinder/Dropbox/my-nodejs-proj ports: - 3000:3000 command: "node ./src/index.js"

其中,比較要注意的是 ports,第一個 3000 是 host 使用的 port,第二個 3000 是 container 使用的 port。 執行使用 up,關閉使用 down:

# 啟動 docker-compose up # 在背景中啟動 docker-compose up -d # 關閉 docker-compose down # 查看正在執行的 containers docker ps

*** 在啟動專案的時候出錯了「invalid ELF header」,無法載入 bcrypt 模組。 問題出在,bcrypt 是平台相依性的,我的 host 是 MacOS,container 則是 Debian。

目前想到的解法是,重新建立 image 檔,將專案內的檔案(排除 node_modules)複製到 container,然後再重新 npm install 所有套件。要設定三個檔案:.dockerignore,Dockerfile,docker-compose.yml。

# .dockerignore node_modules
# Dockerfile FROM node:10 RUN npm install -g nodemon RUN mkdir -p /usr/src/app WORKDIR /usr/src/app COPY package.json /usr/src/app RUN npm install EXPOSE 3000
# docker-compose.yml version: "3" services: node: build: . working_dir: /usr/src/app environment: - NODE_ENV=development volumes: - ./:/usr/src/app - /usr/src/app/node_modules # 上式可以用 container 的路徑覆蓋原來 host 的 node_modules 目錄 ports: - 3000:3000 command: "npm run dev"

執行 docker-compose up 即可建立,若調整設定檔後要重建可使用 docker-compose build。 其它常用的指令:

# 連入 container 的 bash docker exec -it <container-id> /bin/bash # 列出所有 image 檔 docker image ls # 移除某個 image 檔 docker rmi <image-id>

2020-11-07

使用 mongoose 連線遠端 mongodb (ubuntu)

使用 mongoose 連線遠端 mongodb (ubuntu)

通常連線遠端的 Mongodb server,使用 connection string uri:

mongodb://[username:password@]host1[:port1][,...hostN[:portN]][/[defaultauthdb][?options]]

Admin 帳號設定方式同 這篇。但是在使用 mongoose 連線時,一般的 connection uri 設定一直無法正常連線。

爬了文才發現要在 mongoose 連線時,使用 auth, user, pass 三個另外的設定,uri 內就不用放帳號和密碼了。例如我的連線檔案 mongoose-connect.js:

const mongoose = require('mongoose'); mongoose.connect(process.env.MONGO_DSN, { useNewUrlParser: true, useUnifiedTopology: true, useCreateIndex: true, useFindAndModify: false, "auth": { "authSource": "admin" }, "user": process.env.MONGO_USER, "pass": process.env.MONGO_PASS, }); const db = mongoose.connection; db.on('error', (error)=>{ console.log('Mongoose connection error:', error); console.log(`mongoose.connection.readyState: ${mongoose.connection.readyState}`); }); db.on('open', ()=>{ console.log('Mongoose connected ~'); }); module.exports = mongoose;

2020-11-03

ubuntu 備份 mysql

ubuntu 備份 mysql

一般使用 mysqldump 來備份 mysql 資料

# 只備份 schema mysqldump --no-data -u db_user -p'db_password' db_name > mybackup.sql

如果不想把 db_password 放在裡面,可以建立 ~/.my.cnf 輸入如下,並將檔案屬性設定為 600:

[mysqldump] user=db_user password=db_password

把要備份的檔案加上日期,將工作寫成 backup.sh:

#!/bin/bash today=`date +"%Y-%m-%d"` mysqldump --no-data db_name > "mybackup-${today}.sql"

搭配 crontab 使用,即可自動排程備份。以下是備份後複製到遠端主機,然後移除檔案:

#!/bin/bash today=`date +"%Y-%m-%d"` sqlFile="/root/db_bk/mydatabase-${today}-all.sql" mysqldump mydatabase > "${sqlFile}" scp -C "${sqlFile}" root@xxx.xxx.xxx.xxx:/mnt/volume_somewhere/db_bk/ rm "${sqlFile}" echo "ok"

2020-11-01

nginx + nodejs 使用 certbot

nginx + nodejs 使用 certbot

以下的設定參考 教學-申請Let’s Encrypt憑證與啟用https

承上篇,在 nginx 設定中加入一個 server /etc/nginx/sites-enabled/www.shinder.cc,並設定為:

server { listen 80; server_name www.shinder.cc shinder.cc; location / { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Host $host; } }

啟動 node/express server,使用 port 3000。 然後測試 http://www.shinder.cc 是否可以看到 node/express 的執行內容。 安裝 certbot:

# 先加入 certbot 的 repo 資料 sudo add-apt-repository ppa:certbot/certbot # 更新 repo 並安裝 sudo apt update sudo apt install certbot

申請憑證:

sudo certbot certonly --webroot --webroot-path=/home/ubuntu/<express專案>/public/ -d www.shinder.cc -d shinder.cc

接著會有幾個詢問, email,是否同意授權,email是否要接收訊息等等。最後就是 key 所放的位置:

/etc/letsencrypt/live/www.shinder.cc/fullchain.pem /etc/letsencrypt/live/www.shinder.cc/privkey.pem

為了增加安全性產生一個2048-bit Diffie-Hellman的密碼組合:產生的 /etc/ssl/certs/dhparam.pem 之後會用到:

sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048

更改 nginx 設定

建立設定檔 /etc/nginx/snippets/ssl-www.shinder.cc.conf

ssl_certificate /etc/letsencrypt/live/www.shinder.cc/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/www.shinder.cc/privkey.pem;

建立另一個設定檔 /etc/nginx/snippets/ssl-params.conf

ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH"; ssl_ecdh_curve secp384r1; ssl_session_cache shared:SSL:10m; ssl_session_tickets off; ssl_stapling on; ssl_stapling_verify on; resolver 8.8.8.8 8.8.4.4 valid=300s; resolver_timeout 5s; add_header X-Frame-Options DENY; add_header X-Content-Type-Options nosniff; ssl_dhparam /etc/ssl/certs/dhparam.pem;

最後修改 nginx 設定檔 /etc/nginx/sites-enabled/www.shinder.cc

server { listen 443 ssl http2; listen [::]:443 ssl http2; include snippets/ssl-www.shinder.cc.conf; include snippets/ssl-params.conf; server_name www.shinder.cc shinder.cc; client_max_body_size 1G; location / { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Host $host; } } server { listen 80; server_name www.shinder.cc shinder.cc; return 301 https://$server_name$request_uri; }

nginx 重新載入設定就可以測試 https 了。

自動更新憑證

使用 crontab 排程:

# 進入編輯 sudo crontab –e

加入下行,每日早上七點更新:

10 7 * * * /usr/bin/certbot renew --quiet --renew-hook "/bin/systemctl reload nginx"

如果 web document root 變更了,可以在 /etc/letsencrypt/renewal/www.shinder.cc.conf 檔案內修改 webroot_map 。

在 AWS EC2 安裝 nginx

在 AWS EC2 安裝 nginx

安裝 nginx 和 php

前置的步驟:

  • 先登入 AWS console。
  • 選擇 EC2 console。
  • 啟用實體(instance)。
  • 選擇作業系統影像檔,我習慣用 ubuntu。
  • 選虛擬機器規格,一般用途選 t2 系列。
  • 啟動之前會詢問要使用已存在的 key pair 或者建立新的。若選建立新的,要保管好 key,不然無法登入實體。
  • 啟動後可以在 console 看到實體。

注意:記得到 instance console 的 security 分頁,設定 inbound rules,讓一般人都可以透過 http 和 https 拜訪。

登入主機:我自己是使用 Mac zsh,當然也可以使用 linux bash,若使用 Windows 建議用 git bash 來登入。

# 先將 key 檔權限設定為 400 chmod 400 my_ec2_key.pem # 使用 ssh 登入,ubuntu 為預設用戶 ssh -i /my_path/my_ec2_key.pem ubuntu@xxx.xxx.xxx.xxx

將本機的 ~/.ssh/id_rsa.pub 加到遠端 ~/.ssh/authorized_keys 內,之後就可以不需要 pem 檔登入。

# 登入後先更新系統 sudo apt update sudo apt upgrade
# 安裝 nginx sudo apt install nginx # 查看 nginx 狀態,應該會是啟動的狀態 service nginx status
# 利用 curl 測試是否可以正常訪問 curl -I http://xxx.xxx.xxx.xxx
# 安裝 PHP 相關套件 sudo apt install php php-cli php-fpm php-json php-pdo php-mysql php-zip php-gd php-mbstring php-curl php-xml php-pear php-bcmath # 安裝套件後會自動啟動 apache2,但由於未安裝 apache2 所以會看到啟動失敗的錯誤訊息 # 查看 php-fpm 的狀態,應該要為啟動的狀態 service php7.4-fpm status
# 查看 nginx 預設網站設定,原則上不需要變更 sudo nano /etc/nginx/sites-available/default

在 /var/www/ 建立「 網域同名資料夾」,例如 php.shinder.cc,用來服務 php.shinder.cc domain 的站台。 在 /etc/nginx/sites-available/ 新增一個設定檔(通常和你的網域同名),例如: php.shinder.cc。並設定內容如下:

server { listen 80; server_name php.shinder.cc; root /var/www/php.shinder.cc; index index.php index.html; location / { try_files $uri $uri/ =404; } location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/var/run/php/php7.4-fpm.sock; } location ~ /\.ht { deny all; } }
# 在 sites-enabled 內建立 link sudo ln -s /etc/nginx/sites-available/php.shinder.cc /etc/nginx/sites-enabled/
# 測試一下 nginx 的設定檔,看有沒有寫錯 sudo nginx -t
# 重啟 nginx sudo service nginx restart

在 /var/www/php.shinder.cc 放一個 phpinfo 的測試程式,就可以測試 php 了。

安裝 nodejs

# 目前從官方套件安裝,版本為 10.19 有點舊,不過應該可以應付大部份的需求 sudo apt install nodejs npm

2020-10-31

在 Digitalocean Spaces 使用 s3cmd

在 Digitalocean Spaces 使用 s3cmd

上一篇 基本上是廢文。 s3fs 好用,但只適合用在檔案數不多的情況下操作,而且不適合搭配 rsync 一起使用。

檔案數多時(我的狀況是有數百萬個檔案要上傳,或同步),就應該使用 s3cmd。

s3cmd 安裝方式可以參考 Digitalocean 官方的說明 Setting Up s3cmd 2.x with DigitalOcean Spaces

s3cmd 基本用法也可以參考 Examples of s3cmd 2.x Usage with DigitalOcean Spaces。當然也可以用 s3cmd --help 查詢。

以下列出自己常用的部份:

# 上傳 my_path2 整個資料夾到 s3://my_bucket/my_path2,同時設定為 public s3cmd put --acl-public --recursive /my_path1/my_path2 s3://my_bucket/
# 同步 my_path2 資料夾到 s3://my_bucket/my_path2,同時設定為 public s3cmd sync --acl-public /my_path1/my_path2 s3://my_bucket/
# 設定 s3://my_bucket/my_path2 內所有檔案為 public s3cmd setacl --acl-public --recursive s3://my_bucket/my_path2/

2020-10-25

在 Digitalocean Spaces 和 Wasabi 上使用 s3fs 和 rsync

在 Digitalocean Spaces 和 Wasabi 上使用 s3fs 和 rsync

AWS S3 是個方便的雲端工具,唯一的缺點就是價格高了一些(消費者的缺點?) 剛好有客戶是用 digitalocean 的空間,就來試試 Spaces 吧(開始踩雷之旅)。

之前試用 S3,發現 s3fs 真是個好東西,掛上去之後就像使用一般磁碟一樣方便。

在 ubuntu 20.4 上安裝好 s3fs 後,Spaces 的設定一直搞不定,參考了這篇How to mount DigitalOcean Spaces on droplets with s3fs

其中一點是 /etc/fuse.conf 裡的 user_allow_other 要開啟。 另外要開 debug mode 的設定,不然永遠都沒訊息,不管有沒有設定成功。

-o dbglevel=info -f -o curldbg

Digitalocean Spaces 以設定在 sfo2 為例子:

s3fs mySpacesName ${HOME}/s3/myMntFolder -o passwd_file=${HOME}/.passwd-do-spaces -o url=https://sfo2.digitaloceanspaces.com/ -o allow_other -o use_path_request_style -o dbglevel=info -f -o curldbg

上式中,mySpacesName 為 Spaces 名稱;${HOME}/s3/myMntFolder 為掛載的資料夾路徑;.passwd-do-spaces 為金鑰的設定檔。測試後,除錯的參數就可以拿掉了。

Wasabi Storage 的設定:

s3fs myBucketName ${HOME}/s3/myMntFolder -o passwd_file=${HOME}/.passwd-wasabi -o url=https://s3.us-west-1.wasabisys.com

Wasabi 要注意的是,url 的設定。官方說使用 https://s3.wasabisys.com 就可以了,但實際上會出錯(不知道是不是 s3fs 版本的問題)。應該直接使用 endpoint(雲端可用區域)的 url。

原以為掛上去之後就會帆風順,結果原本好用的 rsync 卻一直出狀況。同步上傳沒多少檔案就出錯罷工,之後常常看到隱藏檔的出現,但來源卻沒有這些檔案。

找了好一陣子才發現這篇 S3FS rsync recommendations,主要是針對 rsync 的特性和 S3 的特性做參數的校調。

例如:

rsync -avW --progress --inplace --size-only /mnt/volumes/my_copy_folder /target_folder

目前看來,似乎可以好好用 rsync。 還是有個問題:Digitalocean Spaces 和 Wasabi Storage 上傳的速度好慢...



備註:之後改用 s3cmd 上傳,速度快很多。

2020-10-24

在 ubuntu 20.4 安裝 php 5.6

 在 ubuntu 16.4 之後的版本已經不支援 php 5 了。

目前還是有解決的方式:

https://stackoverflow.com/questions/36788873/package-php5-have-no-installation-candidate-ubuntu-16-04


找到已安裝的 php:

dpkg -l | grep php| awk '{print $2}' |tr "\n" " "

移除已安裝的 php:

sudo aptitude purge `dpkg -l | grep php| awk '{print $2}' |tr "\n" " "`

加入 apt repo:

sudo add-apt-repository ppa:ondrej/php

安裝:

sudo apt-get update
sudo apt-get install php5.6



2020-10-12

安裝 laravel 8 和 jetstream

安裝 laravel 8 和 jetstream

安裝 laravel 8 之前先更新 laravel/installer,應該要是 4.0 版以上:

composer global require laravel/installer

接著就可以建立專案,同時使用 --jet 安裝 jetstream:

laravel new project-name --jet

安裝時要選擇使用 livewire 或 inertia,我個人選了 inertia。 另一個問題是要不要使用 teams,測試專案所以選了否。 接著做 migrate:

php artisan migrate

在這裡遇到 1071 Specified key was too long 的問題。 建議使用 MySQL 5.7 以上,我用的是 5.5。在 AppServiceProvider.php 內設定 defaultStringLength 為 191 可以修正此問題。

use Illuminate\Support\Facades\Schema; public function boot() { Schema::defaultStringLength(191); }

啟動測試 Server:

php artisan serve

使用 php artisan route:list 查看路由時,可以看到設定的內容,但在 /routes/web.php 卻看不到相關的設定,要變更路由會是一大問題。 在這篇 laravel-8-jetstream-login-route 有提到設定方式,在 /config/fortify.php 內加入 path 的設定:

'path' => 'admin',

如此可以將 /login 變更成 /admin/login。

2020-07-03

前端的兩個狀況

同學:「老蘇對 localStorage 熟嗎?」
老蘇:「localStorage 有什麼好熟的?」
...
結果問題果然不是在 localStorage

- - - - - - - - - 

localStorage 是個輔助功能,當快取用很方便,而且資料不會因為刷頁面而消失。
但 localStorage 並不能取代 cookie 或 session 的功能。
許多安全性相關的資料不應該放在 localStorage。
不要為了方便而失去安全。

- - - - - - - - - 

另一個狀況是用 fetch() 發 AJAX
在某個 react component 裡,那個 AJAX 總是 pending 的狀態,要不到資料,但也不噴錯。
後端 API 確定是正常的。
後來學員自己發現,發 AJAX 後有一段落入的無窮迴圈,以至於 AJAX 一直無法處理完成。

2020-05-17

Raspbian 安裝 MariaDB

Raspbian 安裝 MariaDB

查看 Raspbian 提供支援 MariaDB 的版本:

pi@raspberrypi:~ $ apt-cache policy mariadb-server mariadb-server: 已安裝:(無) 候選: 1:10.3.22-0+deb10u1 版本列表: 1:10.3.22-0+deb10u1 500 500 http://raspbian.raspberrypi.org/raspbian buster/main armhf Packages

安裝:

sudo apt install mariadb-server

安裝完成後,使用下式連線:

sudo mysql -u root

如果有需要,建立外連的用戶:

-- 建立本地端可以連線的用戶 CREATE USER 'newuser'@'localhost' IDENTIFIED BY 'newuser_pw'; GRANT ALL PRIVILEGES ON * . * TO 'newuser'@'localhost'; FLUSH PRIVILEGES; -- 建立其他主機可以連線的用戶 CREATE USER 'newuser2'@'%' IDENTIFIED BY 'newuser_pw2'; GRANT ALL PRIVILEGES ON * . * TO 'newuser2'@'%'; FLUSH PRIVILEGES;

從 mac 用 adminer.php(Apache/PHP 環境)連到 R-Pi,結果得到 connection refused,查看網路狀況:

sudo netstat -ln

得到:

# sudo netstat -ln Proto Recv-Q Send-Q Local Address Foreign Address State tcp 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN

爬文得知要設定 bind-address=0.0.0.0,到 /etc/mysql/my.cnf 設定結果無效。 後來才找到在 /etc/mysql/mariadb.conf.d/50-server.cnf 有設定 bind-address=127.0.0.1 ,變更之後,重新啟動:

sudo service mysqld restart

就可以由外部連入:

# sudo netstat -ln Proto Recv-Q Send-Q Local Address Foreign Address State tcp 0 0 0.0.0.0:3306 0.0.0.0:* LISTEN

2020-05-16

在 Raspbian 安裝 MongoDB 的慘痛經驗 (廢文)

在 Raspbian 安裝 MongoDB 的慘痛經驗 (廢文)

由於 Raspberry Pi 4 model B 的 CPU ARMv7l 為 32 bit,所以只能安裝舊版的 MongoDB。若是網路架構,可以將 MongoDB 裝在別的主機或雲端,使用較新版本的 MongoDB。

在 Raspbian 中查看提供的 MongoDB 版本就好,不要安裝:

pi@raspberrypi:~ $ apt-cache policy mongodb mongodb: 已安裝:(無) 候選: 1:2.4.14-4 版本列表: 1:2.4.14-4 500 500 http://raspbian.raspberrypi.org/raspbian buster/main armhf Packages

只提供 2.4 的版本真的太老舊了,官方 drivers 都不支援了,真的別裝!

如果使用 MongoDB 3.2 以上是必要的那就請用 Ubuntu 20.04 LTS (64-bit),目前已經可以透過 Imager 安裝這個版本了。

以下是做個記錄,不要實作,在 Raspbian,MongoDB 3.2 和 3.0 32-bit 都無法執行,殘念。

支援 32-bit MongoDB 的最高版本 3.2.22,請參考這篇

可以透過瀏覽器到 MongoDB 官網下載中心 選取「Version 3.2.22」>「Linux 32-bit lagecy」>「TGZ」下載(拷備表單下的連結也可以)。

wget https://fastdl.mongodb.org/linux/mongodb-linux-i686-3.2.22.tgz # 下載 tar -zxvf mongodb-linux-i686-3.2.22.tgz # 解壓縮 sudo cp ./mongodb-linux-i686-3.2.22/bin/* /usr/bin/ # 複製檔案

設定啟動檔

wget https://raw.githubusercontent.com/mongodb/mongo/master/debian/init.d # 下載啟動檔 sudo mv init.d /etc/init.d/mongod # 移動檔案 sudo chmod 755 /etc/init.d/mongod # 變更屬性

建立設定檔 /etc/mongod.conf

storage: dbPath: /var/lib/mongo journal: enabled: true engine: mmapv1 systemLog: destination: file logAppend: true path: /var/log/mongodb/mongod.log processManagement: fork: true net: port: 27017 bindIp: 0.0.0.0

建立 mongodb 用戶

sudo useradd --home-dir /var/lib/mongo --shell /bin/false mongodb sudo passwd mongodb

建立存放資料的資料夾 /var/lib/mongo

# 存放資料庫資料 sudo mkdir /var/lib/mongo sudo chown -R mongodb /var/lib/mongo sudo chgrp -R mongodb /var/lib/mongo # 記錄檔 sudo mkdir /var/log/mongodb sudo chown -R mongodb /var/log/mongodb sudo chgrp -R mongodb /var/log/mongodb # pid sudo touch /var/run/mongod.pid sudo chown mongodb /var/run/mongod.pid sudo chgrp mongodb /var/run/mongod.pid

初始化服務:

sudo update-rc.d mongod defaults

最後重開機,然後還是不能執行。

Raspberry Pi 4 model B 安裝 Ubuntu 64-bit

Raspberry Pi 4 model B 安裝 Ubuntu 64-bit

Raspberry Pi 4 B 的 CPU 為 ARMv7l,是 32-bit 的 CPU,一般只會安裝 32-bit 的 OS。

但有些軟體已經不再支援 32-bit 的環境,此時使用 64-bit 變成一個基本需求。可喜的是 Ubuntu 官方已經提供 64-bit for Raspberry Pi 的 OS,不需要再到網路上找複雜的處理方式。

先到 Ubuntu 官網 下載 for Raspberry Pi 的 image 檔,現在可以選 Ubuntu 20.04 64-bit 的版本。雖然 R-Pi 官方的 Imager 裡已經有提供 Ubuntu 64-bit 的選項,但是還是建議先下載再使用 Imager,因為下載速度真的很慢。

接著再依照 How to install Ubuntu on your Raspberry Pi 安裝即可。

我個人是直接接實體網路線,所以 wifi 的設定都省了。

查看目前系統中可以安裝的 nodejs 和 mongodb 版本:

ubuntu@ubuntu:~$ apt-cache policy nodejs nodejs: Installed: (none) Candidate: 10.19.0~dfsg-3ubuntu1 Version table: 10.19.0~dfsg-3ubuntu1 500 500 http://ports.ubuntu.com/ubuntu-ports focal/universe arm64 Packages ubuntu@ubuntu:~$ apt-cache policy mongodb mongodb: Installed: (none) Candidate: 1:3.6.9+really3.6.8+90~g8e540c0b6d-0ubuntu5 Version table: 1:3.6.9+really3.6.8+90~g8e540c0b6d-0ubuntu5 500 500 http://ports.ubuntu.com/ubuntu-ports focal/universe arm64 Packages

如果沒有要太新的功能 nodejs 10 是可以接受的,安裝 nodejs 和 npm:

sudo apt install nodejs sudo apt install npm

也可以直接裝 MongoDB 3.6。若要安裝 MongoDB 4.2 可以參考這篇 Install Node.js and Npm on Raspberry Pi

2020-05-15

在 Raspbian 上安裝 VSCode

在 Raspbian 上安裝 VSCode

已經有人寫好 shell scripts 了 https://code.headmelted.com/

稍微改一下裡面的做法:

wget https://code.headmelted.com/installers/apt.sh # 下載 scripts chmod +x ./apt.sh # 加入「可執行」屬性 sudo ./apt.sh # 以管理者權限執行

裝好之後可以在圖形化介面的「啟動」>「Programming」>「Code-OSS (headmelted)」啟動 VSCode,也可以在 terminal 用 code-oss 去開啟一個文字檔。

2020-05-14

在 Raspbian 上安裝 Node.js

在 Raspbian 上安裝 Node.js

參考 Install Node.js and Npm on Raspberry Pi

先在 terminal 上查看 Raspberry Pi 的 CPU 型號:

uname -m

我的機器是 Pi 4 model B,CPU 是 ARMv7l。

Node.js 下載頁面 找到 Linux Binaries (ARM) 的版本,目前 LTS 12.16.3 版提供 ARMv7 和 ARMv8。若要找支援 ARMv6 就要找比較舊的版本。複製連結,並使用 wget 下載。

wget https://nodejs.org/dist/v12.16.3/node-v12.16.3-linux-armv7l.tar.xz

解壓縮檔案:

# tar -xvf 檔名.tar.xz tar -xvf ./node-v12.16.3-linux-armv7l.tar.xz

將檔案複製到 /usr/local/ 裡:

cd node-v12.16.3-linux-armv7l/ sudo cp -R * /usr/local/

最後查看版本,沒問題的話就完成安裝了:

node -v npm -v

這種安裝方式的缺點是,如果要移除 Node.js,由於沒有記錄複製的檔案,會無法移除所有檔案。

另一種安裝方式是使用 NodeSource Distributions,這個工具是由 NodeSource 這家公司所提供。

目前支援 Debian OS 的版本中,對 Raspberry Pi 支援的 CPU 只有 ARMv7 和 ARMv8,舊的 ARMv6 就不適合使用 NodeSource 的方案。以下是安裝的步驟:

先更新系統套件:

sudo apt-get update sudo apt-get dist-upgrade

NodeSource Distributions 查看支援 Debian 的版本,以下是安裝 12.x 的範例:

# Using Debian, as root curl -sL https://deb.nodesource.com/setup_12.x | bash - apt-get install -y nodejs

最後查看 Node 版本確認即可。

2020-05-13

透過 mac 安裝 Raspbian

透過 mac 安裝 Raspbian

首先到 Raspberry Pi 官網 下載 Raspberry Pi Imager for macOS,將 Raspberry Pi Imager.app 裝起來。

啟動 Imager 後,可以先格式化 SD 卡。

  1. Operating System 選 Erase
  2. SD Card 選讀卡設備
  3. 點選「Write」

安裝 Raspbian,也是同樣的步驟,不過下載檔案需要一些時間:

  1. Operating System 選「Raspbian」
  2. SD Card 選讀卡設備
  3. 點選「Write」

用我的老 mac mini 花了快一個小時... 後來發現是下載速度過慢的問題。 之後就可以裝 SD Card 試開機,一開始接比較舊的電視發現沒有畫面,換一般電腦螢幕就正常了。

一開始設定最重要的是開啟 SSH 和 VNC。有 SSH 才可以遠端登入 terminal,VNC 則是遠端桌面。 經常還是會發生設定上的問題,那可以用 ssh 登入:

ssh pi@192.168.1.108

輸入密碼後即可操作 Raspian 的 terminal。使用下式來設定:

sudo raspi-config

首頁畫面的 Interfacing Options 進入後可以啟動 SSH 和 VNC。 首頁畫面的 Advanced Options > Resolution,可以調整使用的解析度。有時 VNC 連線有狀況時,可以設定一個較小的模式,方便 VNC 呈現畫面。(CEA 模式比較接近電視,DMT 則是比較接近電腦螢幕)

2020-05-10

以 Flask / MongoDB 製作 通訊錄 CRUD

以 Flask / MongoDB 製作 通訊錄 CRUD

程式碼太多,請參考本文的專案 https://github.com/shinder/flask-practice,以下只列出部份程式碼:

主程式:

import modules.address_book app.add_url_rule('/address-book/list/', None, modules.address_book.ab_list) app.add_url_rule('/address-book/list/<int:page>', None, modules.address_book.ab_list) app.add_url_rule('/address-book/edit/<_id>', None, modules.address_book.ab_edit_get, methods=['GET']) app.add_url_rule('/address-book/edit', None, modules.address_book.ab_edit_post, methods=['POST']) app.add_url_rule('/address-book/add', None, modules.address_book.ab_add_get, methods=['GET']) app.add_url_rule('/address-book/add', None, modules.address_book.ab_add_post, methods=['POST']) app.add_url_rule('/address-book/delete/<_id>', None, modules.address_book.ab_delete)

app/modules/address_book.py 的內容:

from flask import Flask, request, render_template, session, jsonify, redirect from bson.objectid import ObjectId import json import math import re import modules.mongo_connection email_pattern = r"^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$" def ab_list(page=1): (db, connection) = modules.mongo_connection.getDB('test') output = { 'page_name': 'ab-list', 'page_title': '列表 - 通訊錄', 'totalRows': 0, # 總筆數 'perPage': 3, # 每一頁最多幾筆 'totalPages': 0, # 總頁數 'page': page, # 用戶要查看的頁數 'rows': [], # 當頁的資料 } output['totalRows'] = db.address_book.count_documents({}) output['totalPages'] = math.ceil(output['totalRows']/output['perPage']) output['page'] = 1 if page < 1 else page if output['page'] > output['totalPages']: output['page'] = output['totalPages'] if output['totalRows']==0: output['rows'] = [] else: cursor = db.address_book.find({}, sort=[('_id', -1)], skip=(output['page']-1) * output['perPage'], limit=output['perPage'] ) for doc in cursor: doc['_id'] = str(doc['_id']) output['rows'].append(doc) return render_template('address-book/list.html', **output) def ab_edit_get(_id): (db, connection) = modules.mongo_connection.getDB('test') try: oid = ObjectId(_id) except: return redirect("/address-book/list/1", code=302) row = db.address_book.find_one({'_id': oid}) row['_id'] = _id # 使用字串 if not row: return redirect("/address-book/list/1", code=302) else: row['page_name'] = 'ab_edit' row['page_title'] = '修改 - 通訊錄' return render_template('address-book/edit.html', **row) def ab_edit_post(): output = { 'success': False, 'error': '', } if len(request.form.get('name')) < 2: output['error'] = '姓名字元長度太短' return output email_match = re.search(email_pattern, request.form.get('email'), re.I) if not email_match: output['error'] = 'Email 格式錯誤' return output (db, connection) = modules.mongo_connection.getDB('test') doc = request.form.to_dict() _id = doc['_id'] del doc['_id'] rr = db.address_book.replace_one({'_id': ObjectId(_id)}, doc) # print(rr) # pymongo.results.UpdateResult if rr.modified_count==1: output['success'] = True else: output['error'] = '資料沒有變更'; return output def ab_add_get(): output = { 'page_name': 'ab_add', 'page_title': '新增 - 通訊錄', } return render_template('address-book/add.html', **output) def ab_add_post(): output = { 'success': False, 'error': '', } if len(request.form.get('name')) < 2: output['error'] = '姓名字元長度太短' return output email_match = re.search(email_pattern, request.form.get('email'), re.I) if not email_match: output['error'] = 'Email 格式錯誤' return output (db, connection) = modules.mongo_connection.getDB('test') doc = request.form.to_dict() rr = db.address_book.insert_one(doc) # print(dir(rr)) # InsertOneResult if rr.inserted_id: output['success'] = True else: output['error'] = '資料沒有新增'; return output def ab_delete(_id): (db, connection) = modules.mongo_connection.getDB('test') try: oid = ObjectId(_id) except: return redirect("/address-book/list/1", code=302) rr = db.address_book.delete_one({'_id': oid}) # print(dir(rr)) # DeleteResult referer = request.headers.get('referer') if not referer: return redirect("/address-book/list/1", code=302) else: return redirect(referer, code=302)

2020-05-09

Flask 連線 MongoDB

Flask 連線 MongoDB

本文的參考專案 https://github.com/shinder/flask-practice

這裡直接用最基本的 PyMongo 連線方式,官方文件

建立連線模組,app/modules/mongo_connection.py:

from pymongo import MongoClient url = 'mongodb://localhost:27017' client = None def getDB(db_name): global client if not client: client = MongoClient(url) return (client[db_name], client)

這個 MongoDB 連線模組的想法和 mysql_connection.py 的想法相同,有使用才會連線。測試的 route 部份:

import modules.mongo_connection @app.route('/try-mongo') def try_mongo(): (db, c) = modules.mongo_connection.getDB('test') one = db.inventory.find_one() one['_id'] = str(one['_id']) # 將 ObjectId 轉換為字串顯示 return one

2020-05-08

Flask 新增資料到 MySQL

Flask 新增資料到 MySQL

本文的參考專案 https://github.com/shinder/flask-practice

承上篇,這裡要使用 Postman POST 傳送 JSON 文件,然後 Flask 接收後寫入資料庫。

MySQL 官網新增資料的範例

Flask route 寫法:

@app.route('/receive-json', methods=['POST']) def receive_json(): (cursor, cnx) = modules.mysql_connection.get_cursor() data = json.loads(request.get_data()) # JSON 字串轉換為 dict p = {} sids = [] # 用來記錄新增的 primary key p['name'] = data['name'] if 'name' in data else '' p['email'] = data['email'] if 'email' in data else '' p['mobile'] = data['mobile'] if 'mobile' in data else '' p['birthday'] = data['birthday'] if 'birthday' in data else '1900-01-01' p['address'] = data['address'] if 'address' in data else '' # 兩種作法 sql1 = ("INSERT INTO `address_book`" "(`name`, `email`, `mobile`, `birthday`, `address`, `created_at`" ") VALUES (%s, %s, %s, %s, %s, NOW())") sql2 = ("INSERT INTO `address_book`" "(`name`, `email`, `mobile`, `birthday`, `address`, `created_at`" ") VALUES (%(name)s, %(email)s, %(mobile)s, %(birthday)s, %(address)s, NOW())") cursor.execute(sql1, (p['name'], p['email'], p['mobile'], p['birthday'], p['address'])) sids.append(cursor.lastrowid) # 取得新增項目的 primary key cursor.execute(sql2, p) # 使用 dict sids.append(cursor.lastrowid) cnx.commit() # 提交新增的資料才會生效 return jsonify(sids) # 輸出 JSON 格式

Postman 發需求的網址 http://localhost:5000/receive-json,JSON 文件如下:

{ "address": "台南市", "birthday": "2000-11-22", "email": "wwww@test.com", "mobile": "0918777-777", "name": "陳小華" }

Flask 使用 MySQL Connector

Flask 使用 MySQL Connector

本文的參考專案 https://github.com/shinder/flask-practice

專案資料表 address_book 參考

連線 MySQL DB 的套件,這邊介紹最陽春的,就是 MySQL 官方出的 mysql-connector。可以依照 mysql-connector 開發人員指引 介紹的方式安裝。用 pip 安裝應該是最簡單的:

pip install mysql-connector

連線的功能我們把它獨立出來成為一個模組 app/modules/mysql_connection.py,其中 get_cursor() 可以同時回傳游標物件和連線物件:

import mysql.connector connect_data = { 'host': 'localhost', 'user': 'root', 'passwd': 'root', 'database': 'test' } cnx = None def get_connection(): global cnx # 將連線物件存放在全域變數 if not cnx: cnx = mysql.connector.connect(**connect_data) return cnx else: return cnx def get_cursor(): cursor = get_connection().cursor(dictionary=True) # 讀出資料使用 dict,預設為 tuple return (cursor, get_connection()) # 同時回傳 cursor 和 connection

在主檔案定義 route:

import modules.mysql_connection @app.route('/try-mysql') def try_mysql(): (cursor, cnx) = modules.mysql_connection.get_cursor() sql = ("SELECT * FROM address_book") cursor.execute(sql) return render_template('data_table.html', t_data=cursor.fetchall())

樣版檔 app/templates/data_table.html

<tbody> {% for i in t_data %} <tr> <td>{{ i.name }}</td> <td>{{ i.email }}</td> </tr> {% endfor %} </tbody>

2020-05-07

使用 pip freeze 記錄安裝的套件

使用 pip freeze 記錄安裝的套件

一般會使用 pip freeze 查看和記錄目前專案使用的套件,常用的方式是存入 requirements.txt:

pip freeze > requirements.txt

搬移專案或 git clone 專案到別的地方重新安裝套件時:

pip -r requirements.txt

這種做法的缺點是,無法看到套件相依性的關係。可以另外使用 requirements-top.txt 來記錄手動安裝的套件。 例如,安裝 flask 只要記錄下式,而不用記錄 Jinja2、Werkzeug 等套件:

Flask==1.1.2

2020-05-05

NodeJS 將 session 資料存入 MySQL

NodeJS 將 session 資料存入 MySQL

一般使用 express.js 時,使用的 session 套件為 express-session。使用記憶體存放 session 資料的做法:

const session = require('express-session'); app.use(session({ saveUninitialized: false, resave: false, secret: '你的 cookie 加密字串', cookie: { maxAge: 1200000 // 單位為毫秒 } }));

若要將 session 存入資料庫,需要先安裝 express-mysql-session 套件。設定方式如下,其中的 db_connect2.js 請看 上篇

const session = require('express-session'); const MysqlStore = require('express-mysql-session')(session); const db = require(__dirname + '/db_connect2'); const sessionStore = new MysqlStore({}, db); app.use(session({ saveUninitialized: false, resave: false, secret: '你的 cookie 加密字串', store: sessionStore, cookie: { maxAge: 1200000 } }));

若使用 session 可以在資料庫看到這樣的資料:

CREATE TABLE IF NOT EXISTS `sessions` ( `session_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, `expires` int(11) unsigned NOT NULL, `data` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO `sessions` (`session_id`, `expires`, `data`) VALUES ('8CDH6O91CkkY_1DpJs7h3YmzbqgQeqrF', 1588332706, '{"cookie":{"originalMaxAge":1200000,"expires":"2020-05-01T11:31:42.263Z","httpOnly":true,"path":"/"},"hello":"shinder"}'); ALTER TABLE `sessions` ADD PRIMARY KEY (`session_id`);

2020-05-04

NodeJS 使用 mysql2 連線 MySQL

NodeJS 使用 mysql2 連線 MySQL

Node 連線 MySQL 資料庫,常用的套件為 mysqlmysql2。mysql 是比較資深的套件,但缺點是沒有直接支援 Promise,所以在使用上若要使用 Promise 需要使用 bluebird 之類的套件。

mysql2 標榜更快,支援 Promise。以下為連線的 module ( db_connect2.js ):

const mysql = require('mysql2'); const pool = mysql.createPool({ host: 'localhost', user: 'root', password: 'root', database: 'test', waitForConnections: true, connectionLimit: 10, // 最大連線數 queueLimit: 0 }); module.exports = pool.promise(); // 滙出 promise pool

在 express.js 使用上的例子:

const db = require(__dirname + '/db_connect2'); app.get('/try-db', (req, res)=>{ const sql = "SELECT * FROM address_book LIMIT 3"; db.query(sql).then(([results, fields])=>{ res.json(results); }); });

2020-05-03

Babel-node: 在 Node 上使用全 ES6 語法

Babel-node: 在 Node 上使用全 ES6 語法

目前 Node.js 已經可以使用絕大部份的 ES6 語法來開發,其中不支援的語法主要是 import 和 export。 Node.js 原生只支援 CommonJS 的 require() 和 module.exports 的語法。 若要使用全 ES6 語法開發可以使用 Babel-Node。Babel-node CLI 和 Node CLI 功能一樣,但多了將 ES6 編譯成 ES5 的功能。

    1. 首先要先安裝三個 babel 套件:@babel/core, @babel/node, @babel/preset-env。
npm i @babel/core @babel/node @babel/preset-env
    1. 在專案中建立 babel.config.json
{ "presets": [ "@babel/preset-env" ] }
    1. 接著就可以使用 babel-node 執行 js 程式:
npx babel-node src/index.js

如果使用 nodemon 啟動 express.js 測試專案,可以設定 package.json 中的 scripts:

{ "scripts": { "start": "nodemon --exec babel-node src/index.js" } }

使用 babel-node 啟動感覺比直接使用 node 啟動要來得慢一點,這就看個人決定是否要使用 babel-node 了。

2020-05-02

NodeJS 連線 MongoDB

NodeJS 連線 MongoDB

假設我們有個現成的 node/express 專案,首先安裝 mongodb 套件:

npm i mongodb

官方 mongodb 套件說明 裡面有各版本的教學和 API 文件。 先撰寫可以建立連線並使用某個 DB 的模組,在此檔名為 mdb_connect.js,只要滙出 getDB 方法即可:

const MongoClient = require('mongodb').MongoClient; const url = 'mongodb://localhost:27017'; const dbName = 'test'; let _db; // 存放對應 DB 的物件 const client = new MongoClient(url, {useUnifiedTopology: true}); client.connect() .then(c => { _db = client.db(dbName); // c 同 client }) .catch(error=>{ console.log('Cannot connect the mongodb server!'); console.log(error); }); const getDB = ()=>{ if(!_db) throw new Error('No MongoDB connection!'); return _db; }; module.exports = getDB;

模組被滙入之後,就開始依設定連線,並將 Db 物件存放到 _db 變數內,呼叫 getDB() 即回傳 _db 所指的 Db 物件。 以下為在 express app 下使用的情況:

const getDB = require(__dirname + '/mdb_connect'); app.get('/try-mdb', (req, res)=>{ const mdb = getDB(); mdb.collection('books') .find({}) .toArray() // Cursor 的 toArray() .then((ar)=>{ res.json(ar); }) });

之前 關聯查詢 的用法,另外用了 AggregationCursor 的 forEach() 方法:

app.get('/try-mdb2', (req, res)=>{ const mdb = getDB(); const ar = []; mdb.collection('books') .aggregate([ { $lookup: { from: 'publishers', foreignField: '_id', localField: 'publisher_id', as: 'publisher' } } ]) .forEach(function(doc){ ar.push(doc) }).then(()=>{ res.json(ar); }) });

2020-05-01

MongoDB Collections 之間的關聯性

MongoDB Collections 之間的關聯性

MongoDB 官方關於關聯性的說明,在關聯式資料庫中,表和表之間的關聯是很平常的事情,在 MongoDB 為了方便資料的維護,當然也有易於處理關聯的設計。在此就不談一對一的情況。

官方 以文件參照建構一對多的模型 裡面的例子其實很容易了解,我們就以裡面的範例來討論。

以下是直接使用嵌入子文件的方式來處理 books 中的出版商資料,很明顯的在維護上每次要更動的資料量會很多:

{ title: "MongoDB: The Definitive Guide", author: ["Kristina Chodorow", "Mike Dirolf"], published_date: ISODate("2010-09-24"), pages: 216, language: "English", publisher: { name: "O'Reilly Media", founded: 1980, location: "CA" } } { title: "50 Tips and Tricks for MongoDB Developer", author: "Kristina Chodorow", published_date: ISODate("2011-05-06"), pages: 68, language: "English", publisher: { name: "O'Reilly Media", founded: 1980, location: "CA" } }

另一種是將出版商出版的書籍記錄在出版商的 collection publishers 內。缺點是,若要從書籍去找出版商,這樣子的效能會比較差:

{ name: "O'Reilly Media", founded: 1980, location: "CA", books: [123456789, 234567890, ...] } { _id: 123456789, title: "MongoDB: The Definitive Guide", author: ["Kristina Chodorow", "Mike Dirolf"], published_date: ISODate("2010-09-24"), pages: 216, language: "English" } { _id: 234567890, title: "50 Tips and Tricks for MongoDB Developer", author: "Kristina Chodorow", published_date: ISODate("2011-05-06"), pages: 68, language: "English" }

比較好的做法,就是 books 的 publisher_id 關聯到 publishers 的 _id。

db.publishers.insertOne({ _id: "oreilly", name: "O'Reilly Media", founded: 1980, location: "CA" }) db.books.insertMany([{ _id: 123456789, title: "MongoDB: The Definitive Guide", author: ["Kristina Chodorow", "Mike Dirolf"], published_date: ISODate("2010-09-24"), pages: 216, language: "English", publisher_id: "oreilly" }, { _id: 234567890, title: "50 Tips and Tricks for MongoDB Developer", author: "Kristina Chodorow", published_date: ISODate("2011-05-06"), pages: 68, language: "English", publisher_id: "oreilly" }])

查詢時,使用 aggregate() 和 $lookup$lookup 可以提供同關聯資料庫的 JOIN,方式如下:

db.books.aggregate([ { $lookup: { from: 'publishers', foreignField: '_id', localField: 'publisher_id', as: 'publisher' } } ]).pretty()
  • from: 為要合併查詢的 collection (publishers)。
  • foreignField: 為 publishers 的對應欄位。
  • localField: 為 books 中對應 publishers._id 的欄位。
  • as: 為查詢結果將 publishers 的文件所放入的欄位(暫時,查詢呈現)。

MongoDB 資料格式驗證 (json schema validation)

MongoDB 資料格式驗證 (json schema validation)

MongoDB 雖然在儲存的資料上很有彈性,但在很多時候太彈性的資料反而不好處理。所以在 collections 上也提供了格式驗證的功能,當設定了 $jsonSchema 表示新增的資料或修改後的資料都必須符合設定的格式。

官方的格式驗證說明 的例子來說明,可以在建立 collection 時,設定格式驗證規則:

db.createCollection("students", { validator: { $jsonSchema: { bsonType: "object", // 文件的類型 required: [ "name", "year", "major", "address" ], // 必要欄位 properties: { name: { bsonType: "string", // 欄位類型:字串 description: "說明的文字" }, year: { bsonType: "int", minimum: 2017, // 最小值 maximum: 3017 // 最大值 }, major: { enum: [ "Math", "English", "Computer Science", "History", null ], description: "必須是上列的其中一個" }, gpa: { bsonType: "double", description: "must be a double if the field exists" }, address: { bsonType: "object", required: [ "city" ], // 必要的子欄位 properties: { street: { bsonType: "string", description: "must be a string if the field exists" }, city: { bsonType: "string", "description": "must be a string and is required" } } } } } } })

BSON Types 的官方參考

也可以使用 collMod 指令,變更 collection 的格式驗證規則:

db.runCommand({ collMod: "contacts", validator: { $jsonSchema: { bsonType: "object", required: ["phone"], properties: { phone: { bsonType: "string", description: "must be a string and is required" }, email: { bsonType: "string", pattern: "@mongodb\.com$", description: "must be a string and match the regular expression pattern" }, status: { enum: ["Unknown", "Incomplete"], description: "can only be one of the enum values" } } } }, validationLevel: "moderate", validationAction: "warn" })

使用 db.getCollectionInfos() 可以顯示各個 collections 的詳細訊息。

> db.runCommand({ collMod: "compoundTest", validator: { $jsonSchema: { bsonType: "object", required: ['a', 'b'], properties: { a: { bsonType: "int" }, b: { bsonType: "int" } } } }, validationLevel: "moderate", // 等級 validationAction: "warn" // 沒通過時 }) > db.getCollectionInfos({name: 'compoundTest'})

2020-04-30

MongoDB 索引

MongoDB 索引

MongoDB 和傳統關聯式資料庫概念相念,都離不開 CRUD,裡面也是讀取(查詢)資料是最多變化的,也可能是最複雜的。其他三個動作,只要讀取熟悉了之後,就相對簡單。

讀取資料,經常需要排序,為了效能也有索引的設置,其中包含單欄(single field)索引和複合索引(compound index)。

首先先使用 explain() 查看讀取的策略,如下列(省略了不相干的內容)。在 queryPlanner.winningPlan 可以看到 stage 值為 COLLSCAN,代表 collections scan,也就是逐筆讀取。

> db.inventory.find() { "_id" : ObjectId("5ea4715cda0c749138d46e52"), "item" : "paper", "qty" : 100 } { "_id" : ObjectId("5ea4715cda0c749138d46e53"), "item" : "journal", "quantity" : 25 } { "_id" : ObjectId("5ea4715cda0c749138d46e54"), "item" : "planner", "qty" : 75 } { "_id" : ObjectId("5ea4715cda0c749138d46e55"), "item" : "postcard", "qty" : 45 } > db.inventory.find({item: 'postcard'}).explain() { "queryPlanner" : { "winningPlan" : { "stage" : "COLLSCAN", "filter" : { "item" : { "$eq" : "postcard" } }, "direction" : "forward" } } }

接著我們使用 createIndex() 新增一個 item 欄位的升冪索引。numIndexesAfter 表示加入索引後的索引數量。再使用 explain() 查看讀取的策略,stage 變成 FETCH。原則上使用 item 這欄為條件讀取資料時,速度會加快許多。

> db.inventory.createIndex({item: 1}) { "createdCollectionAutomatically" : false, "numIndexesBefore" : 1, "numIndexesAfter" : 2, "ok" : 1 } > db.inventory.getIndexes() // 查看所有索引 [ { "v" : 2, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "test.inventory" }, { "v" : 2, "key" : { "item" : 1 }, "name" : "item_1", "ns" : "test.inventory" } ] > db.inventory.find({item: 'postcard'}).explain() { "queryPlanner" : { "winningPlan" : { "stage" : "FETCH", "inputStage" : { "stage" : "IXSCAN", "keyPattern" : { "item" : 1 }, "indexName" : "item_1", "isMultiKey" : false, "multiKeyPaths" : { "item" : [ ] }, "isUnique" : false, "isSparse" : false, "isPartial" : false, "indexVersion" : 2, "direction" : "forward", "indexBounds" : { "item" : [ "[\"postcard\", \"postcard\"]" ] } } } } }

組合索引的官方說明 先新增測試的資料,並建立複合索引,使用 {a: 1, b: 1}

> db.compoundTest.insertMany([ ... {a: 10, b: 2}, {a: 10, b: 8}, {a: 10, b: 6}, ... {a: 70, b: 2}, {a: 70, b: 8}, {a: 70, b: 6}, ... {a: 30, b: 2}, {a: 30, b: 8}, {a: 30, b: 6}, ... ]) > db.compoundTest.createIndex({a:1, b:1}) { "createdCollectionAutomatically" : false, "numIndexesBefore" : 1, "numIndexesAfter" : 2, "ok" : 1 } > db.compoundTest.getIndexes() [ { "v" : 2, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "test.compoundTest" }, { "v" : 2, "key" : { "a" : 1, "b" : 1 }, "name" : "a_1_b_1", "ns" : "test.compoundTest" } ]

以下是測試及取得 queryPlanner.winningPlan.stage 的結果:

db.compoundTest.explain().aggregate({$sort:{a: 1}}) // FETCH db.compoundTest.explain().aggregate({$sort:{a: -1}}) // FETCH db.compoundTest.explain().aggregate({$sort:{a: 1, b:1}}) // FETCH db.compoundTest.explain().aggregate({$sort:{a: 1, b:-1}}) // COLLSCAN db.compoundTest.explain().aggregate({$sort:{a: -1, b:-1}}) // FETCH db.compoundTest.explain().aggregate({$sort:{a: -1, b:1}}) // COLLSCAN db.compoundTest.explain().aggregate({$sort:{b: 1}}) // COLLSCAN db.compoundTest.explain().aggregate({$sort:{b: -1}}) // COLLSCAN

由結果可以知道,複合索引的欄位是有優先順序的。我們 a 放在前面,b 放在後面,所以用 a 去排序的時候都是有效的。a 加上 b 時,只有 {a: 1, b:1}{a: -1, b:-1} 是有效的,因為 {a: -1, b:-1} 是設定的相反順序排序。 所有 b 為主要順序欄位的,索引都無法發揮省時的效果,只能逐筆排查詢。

2020-04-29

MongoDB 移除資料

MongoDB 移除資料

承上篇 MongoDB 更新資料,延用相同的 db 和 collections。

官方的文件說明

使用 mongo shell 操作,刪除資料使用 deleteOne() 或 deleteMany():

> db.inventory.find() { "_id" : ObjectId("5ea4715cda0c749138d46e52"), "item" : "paper", "qty" : 100 } { "_id" : ObjectId("5ea4715cda0c749138d46e53"), "item" : "journal", "quantity" : 25 } { "_id" : ObjectId("5ea4715cda0c749138d46e54"), "item" : "planner", "qty" : 75 } { "_id" : ObjectId("5ea4715cda0c749138d46e55"), "item" : "postcard", "qty" : 45 } { "_id" : ObjectId("5ea6f2ab40e9db6af06a231a"), "item" : "notebook1", "qty" : 50 } { "_id" : ObjectId("5ea6f53240e9db6af06a231b"), "item" : "notebook2", "quantity" : 30 } { "_id" : ObjectId("5ea6f53240e9db6af06a231c"), "item" : "notebook3", "quantity" : 13 } > db.inventory.deleteOne({item:/note/}) { "acknowledged" : true, "deletedCount" : 1 } > db.inventory.deleteMany({item:/note/}) { "acknowledged" : true, "deletedCount" : 2 }

MongoDB 更新資料

MongoDB 更新資料

承上篇 MongoDB 新增資料,延用相同的 db 和 collections。

官方的文件說明

使用 mongo shell 操作,更新單筆資料使用 updateOne():

> db.inventory.updateOne( ... { ... item: 'notebook2' ... }, { ... $set: {qty: 30}, ... $currentDate: { lastModified: true } ... }) { "acknowledged" : true, "matchedCount" : 1, "modifiedCount" : 1 }

第一個參數為條件限定,可以使用 $lt 等運算子。 第二個參數的 $set 為所要變更的設定值;$currentDate 為要設定為當下時間的屬性。

更新多筆使用 updateMany():

> db.inventory.updateMany( ... { qty: { $lte: 30 } }, ... { ... $set: { 'size.uom': "in" }, ... $rename: { qty: 'quantity' }, ... $unset: { status: '' }, ... $currentDate: { lastModified: true } ... } ... ) { "acknowledged" : true, "matchedCount" : 3, "modifiedCount" : 3 }

$rename 為變更欄位( 屬性)名稱;$unset 刪除欄位。

replaceOne() 用來取代某個 document:

> db.inventory.replaceOne( ... { item: /notebook/ }, ... { 'hello': 'my-test' } ... ) { "acknowledged" : true, "matchedCount" : 1, "modifiedCount" : 1 }

replaceOne() 的第一個參數通常會使用 _id 或不會重複的欄位來篩選資料。

FB 留言