App Inventor 2 如何连接MySQL数据库(阿里云数据库)

本文 PHP 后端已配套 Node.js 与 Python 版本,见页面中的对应实现;全部后端源码与运行说明。

« 返回首页

由于阿里数据库(Oceanbase)完全兼容 MySQL,因此这里探讨的就是怎么连MySQL。

App Inventor 2 如何连接MySQL数据库

App Inventor 2 不能直接连接 MySQL(手机上的 App 不应该持有数据库账号密码),有两种做法:

  1. 不想自己维护服务器:用 CloudBase 拓展 的 CloudBaseMySQL 组件,操作腾讯云开发托管的 MySQL,支持查询、插入、更新、删除和统计。
  2. 使用自己的 MySQL:用 Web客户端 把查询请求 Post 到你自己的服务器,服务器上的 PHP、Node.js 或 Python 代码代理执行 SQL,本文讲的就是这种做法。

先看效果:

mysql

开发步骤

  1. 将 PHP 代码放在 Web 服务器上
  2. 在应用和 PHP 代码中设置 SQLKEY,它们必须匹配才能使此代码正常工作,以保障安全(注意:请不要使用特殊字符)
  3. 在 php 脚本中设置数据库连接信息
  4. 测试代码

注意事项:

  • 返回 HTTP 代码 200 表示SELECT 查询成功
  • 返回 HTTP 代码 201 表示不是有效的 SELECT 查询
  • 返回 HTTP 代码 400,表示 SQL 查询错误
  • 请注意SQL注入风险,加强php代码的检查逻辑

前端代码块

执行sql语句的代码块,参考如下:

runQuery

服务器响应处理代码块,参考如下:

onResponse

.aia源码下载:

mysql.aia

后端php代码

php参考代码如下:

<?php
/*
 * Written By: ShivalWolf
 * Date: 2011/06/03
 * Contact: Shivalwolf@domwolf.net
 *
 * UPDATE 2011/04/05
 * The code now returns a real error message on a bad query with the mysql error number and its error message
 * checks for magic_quotes being enabled and strips slashes if it is. Its best to disable magic quotes still.
 * Checks to make sure the submitted form is a x-www-form-urlencode just so people dont screw with a browser access or atleast try to
 * Forces the output filename to be JSON to conform with standards
 *
 * UPDATE 2011/06/03
 * Code updated to use the Web Module instead of tinywebdb
 *
 * UPDATE 2013/12/26 and 2014/02/18
 * minor modifications by Taifun, puravidaapps.com
 *
 * UPDATE 2014/07/11
 * mysql API (deprecated) replaced by mysqli by Taifun
 *
 * UPDATE 2015/04/30
 * SELECT logic adjusted (result stored in temp. file removed) by Taifun
 *
 * UPDATE 2016/02/21
 * Bugfix Undefined variable: csv
 */

/************************************CONFIG****************************************/
//DATABSE DETAILS//
$DB_ADDRESS="";
$DB_USER="";
$DB_PASS="";
$DB_NAME="";

//SETTINGS//
//This code is something you set in the APP so random people cant use it.
$SQLKEY="secret";

/************************************CONFIG****************************************/

//these are just in case setting headers forcing it to always expire
header('Cache-Control: no-cache, must-revalidate');

error_log(print_r($_POST,TRUE));

if( isset($_POST['query']) && isset($_POST['key']) ){                                   //checks if the tag post is there and if its been a proper form post
  //set content type to CSV (to be set here to be able to access this page also with a browser)
  header('Content-type: text/csv');

  if($_POST['key']==$SQLKEY){                                                           //validates the SQL key
    $query=urldecode($_POST['query']);
    if(get_magic_quotes_gpc()){     //check if the worthless pile of crap magic quotes is enabled and if it is, strip the slashes from the query
      $query=stripslashes($query);
    }
    $conn = new mysqli($DB_ADDRESS,$DB_USER,$DB_PASS,$DB_NAME);    //connect

    if($conn->connect_error){                                                           //checks connection
      header("HTTP/1.0 400 Bad Request");
      echo "ERROR Database Connection Failed: " . $conn->connect_error, E_USER_ERROR;   //reports a DB connection failure
    } else {
      $result=$conn->query($query);                                                     //runs the posted query
      if($result === false){
        header("HTTP/1.0 400 Bad Request");                                             //sends back a bad request error
        echo "Wrong SQL: " . $query . " Error: " . $conn->error, E_USER_ERROR;          //errors if the query is bad and spits the error back to the client
      } else {
        if (strlen(stristr($query,"SELECT"))>0) {                                       //tests if it's a SELECT statement
          $csv = '';                                                                    // bug fix Undefined variable: csv
          while ($fieldinfo = $result->fetch_field()) {
            $csv .= $fieldinfo->name.",";
          }
          $csv = rtrim($csv, ",")."\n";
          echo $csv;                                                                    //prints header row
          $csv = '';

          $result->data_seek(0);
          while($row = $result->fetch_assoc()){
            foreach ($row as $key => $value) {
              $csv .= $value.",";
            }
            $csv = rtrim($csv, ",")."\n";
          }
          echo $csv;                                                                    //prints all data rows
        } else {
          header("HTTP/1.0 201 Rows");
          echo "AFFECTED ROWS: " . $conn->affected_rows;       //if the query is anything but a SELECT, it will return the number of affected rows
        }
      }
      $conn->close();                                          //closes the DB
    }
  } else {
     header("HTTP/1.0 400 Bad Request");
     echo "Bad Request";                                       //reports if the secret key was bad
  }
} else {
        header("HTTP/1.0 400 Bad Request");
        echo "Bad Request";
}
?>

Node.js 与 Python 后端

两个版本都接收 POST /mysql.php,表单字段仍为 key、query:SELECT 返回 HTTP 200 和含表头的 CSV;写入返回 HTTP 201 和 AFFECTED ROWS: 数量;请求或 SQL 错误返回 HTTP 400。CSV 中的逗号、双引号和换行会正确转义,可交给 Web 客户端的 CSV 积木解析。把原请求网址替换为 http://服务器IP:3000/mysql.php,其余字段保持一致。

先在启动脚本的终端设置连接信息;Windows 可用 PowerShell 的 $env:DB_HOST="127.0.0.1" 等同名环境变量:

export DB_HOST=127.0.0.1 DB_PORT=3306 DB_NAME=你的数据库
export DB_USER=你的数据库用户 DB_PASS=你的数据库密码 SQLKEY=与你的App一致的密钥

这仍是执行客户端 SQL 的教学接口,SQLKEY 不能限制 SQL 权限;请使用受限数据库账号。正式业务应改为固定操作和参数化查询。

Node.js 对应实现

保存为 mysql-query.cjs,在文件所在目录安装依赖并启动:

npm install express@5 mysql2
node mysql-query.cjs

下载 mysql-query.cjs

// npm install express@5 mysql2
const express = require('express');
const mysql = require('mysql2/promise');
const app = express();
const pool = mysql.createPool({host: process.env.DB_HOST || '127.0.0.1', port: Number(process.env.DB_PORT || 3306),
  user: process.env.DB_USER, password: process.env.DB_PASS, database: process.env.DB_NAME, charset: 'utf8mb4'});
app.use(express.urlencoded({extended: false}));
app.post('/mysql.php', async (req, res) => {
  res.set('Cache-Control', 'no-cache, must-revalidate').type('text/csv');
  const {key, query} = req.body || {};
  if (!process.env.SQLKEY || key !== process.env.SQLKEY || typeof query !== 'string') return res.status(400).send('Bad Request');
  try {
    // 表单解析已经 URL 解码一次,不能再 decodeURIComponent,否则 SQL 中的 +、% 会损坏。
    const [rows, fields] = await pool.query(query);
    if (!Array.isArray(rows)) return res.status(201).send('AFFECTED ROWS: ' + rows.affectedRows);
    const csv = value => /[",\r\n]/.test(String(value ?? '')) ? '"' + String(value ?? '').replace(/"/g, '""') + '"' : String(value ?? '');
    const lines = [fields.map(field => csv(field.name)).join(',')];
    for (const row of rows) lines.push(fields.map(field => csv(row[field.name])).join(','));
    res.send(lines.join('\n') + '\n');
  } catch (error) { res.status(400).send('ERROR SQL query failed'); }
});
if (require.main === module) app.listen(Number(process.env.PORT || 3000), '0.0.0.0');
module.exports = {app, pool};

Python 对应实现

保存为 mysql_query.py,在文件所在目录安装依赖并启动:

python3 -m pip install Flask PyMySQL
python3 mysql_query.py

下载 mysql_query.py

# python3 -m pip install Flask PyMySQL
import csv
import io
import os
import pymysql
from flask import Flask, request, Response
app = Flask(__name__)
def connect():
    return pymysql.connect(host=os.environ.get('DB_HOST', '127.0.0.1'), port=int(os.environ.get('DB_PORT', 3306)),
                           user=os.environ['DB_USER'], password=os.environ['DB_PASS'], database=os.environ['DB_NAME'],
                           charset='utf8mb4', autocommit=True)
def reply(text, status=200):
    return Response(text, status=status, mimetype='text/csv', headers={'Cache-Control': 'no-cache, must-revalidate'})
@app.post('/mysql.php')
def query():
    if not os.environ.get('SQLKEY') or request.form.get('key') != os.environ['SQLKEY'] or 'query' not in request.form:
        return reply('Bad Request', 400)
    connection = None
    try:
        connection = connect()
        with connection.cursor() as cursor:
            cursor.execute(request.form['query'])  # 表单只解码一次
            if cursor.description is None: return reply(f'AFFECTED ROWS: {cursor.rowcount}', 201)
            output = io.StringIO(newline='')
            writer = csv.writer(output, lineterminator='\n')
            writer.writerow([field[0] for field in cursor.description])
            writer.writerows(cursor.fetchall())
            return reply(output.getvalue())
    except (pymysql.MySQLError, KeyError):
        return reply('ERROR SQL query failed', 400)
    finally:
        if connection is not None: connection.close()
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=int(os.environ.get('PORT', 3000)))
文档反馈