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'})

FB 留言