我用MongoDB Realm尝试免费创建了一个超级初学者的Android应用的后端处理

首先

summary.png

关于智能手机应用程序的开发,我们已经在这里发布了一篇文章。

我们采用了这篇文章中的内容来应用MongoDB Realm的使用方法。

希望您先查看上面的三篇文章,然后再閱讀本篇文章。

手机应用程序和后端处理

需要使用服务器进行手机应用程序的开发。

搭建本地服务器
创建数据库
发布API
编写Android应用的后端处理
创建Android应用的用户界面

据说,在后端开发方面通常需要付出巨大的开发成本。
在这样的背景下,为了减少后端开发工作量,云服务逐渐增多。

什么是MongoDB Realm?

mongodb_realm02.png

本次使用上述的MongoDB Realm来处理物联网应用的后端操作,具体而言

A. 将通过树莓派获取的传感器数据上传到云数据库。
B. 将上述数据与安卓应用程序(自定义类的实例)同步,以在智能手机上显示。

我将尝试实现这个处理。

所需之物

・All kinds of temperature and humidity sensors (refer to here for supported sensors).
・Raspberry Pi (We will be using Raspberry Pi 3 Model B this time).
・Python execution environment within Raspberry Pi (We will be using the pre-installed Python 3.7.3 this time).
・Development PC (We will be using Windows 10 this time).
・Android Studio (Environment for Android app development).
・Account for MongoDB Atlas (Can be created for free).
(・Android smartphone (for testing purposes))

步骤

按照以下步骤,实施所需的处理。

    1. MongoDB Atlas的初步配置和服务器数据库的创建

 

    1. 从Raspberry Pi向服务器数据库上传数据

 

    1. MongoDB Realm的服务器端初始设置

 

    创建Android应用程序

1, 2对应于前一章节的A(将RaspberryPi上传至云数据库)
3, 4对应于前一章节的B(云数据库与Android应用程序内的数据同步)

1. 在MongoDB Atlas上进行初步设置并创建服务器数据库。

将服务器端的数据库创建在云服务MongoDB Atlas中。

关于MongoDB Atlas的结构

机构>项目>集群>数据库>集合,
这是一种结构。
数据库对应于常规的关系型数据库(DBMS)中的数据库,集合对应于关系型数据库中的表。
集群是MongoDB独有的结构,据此看,它似乎是用于提高分布式处理效率的类别。
机构和项目是Atlas云独有的结构。详细信息未知,但似乎是为团队和项目管理而设计的类别。

注册MongoDB Atlas

01_mongoatlastop.png
02_register.png

创建群集、创建用户、设置白名单

我希望您能够参考这个。

05_selectplan.png

创建用于传感器数据上传的数据库和集合

可以后续从程序中创建,但为了防止误操作,我们手动进行创建。

11_selectcollection.png
14_confirmcollection.png

创建传感器列表集合

将要使用的传感器列表也将以集合的形式进行管理。

16_create_sensor_list_collection1.png
17_create_sensor_list_collection2.png
18_create_sensor_list_collection3.png

各字段含义如下:
no:标号
sensorname:传感器名称
place:安装位置(室内、室外、厨房三种情况)
temperature:温度信息是否存在(真或假)
humidity:湿度信息是否存在(真或假)
aircon:空调开关信息是否存在(仅对Nature Remo为真)
power:功耗信息是否存在(仅对Nature Remo为真)
_partition:用于MongoDB Realm的同步(固定值为”Project HomeIoT”)

确认DB访问方法

我們將為下一節作準備,確認如何從各種程式語言存取資料庫。

08_connect.png
10_dbaccessfromapp.png

2. 将Raspberry Pi上的数据上传至服务器数据库

我希望你們可以根據這篇文章為基礎。
首先,如果你們能夠實現上述文章的內容,那就太好了。

2-1. 重新编写树莓派内的脚本

为了添加将数据上传到MongoDB Atlas的处理,我们将对以下文件进行如下修改:
config.ini
DeviceList.csv
remo.py
sensors_to_spreadsheet.py

配置.ini

添加数据库设置

[Path]
CSVOutput = CSV出力先を指定
LogOutput = ログ出力先を指定

[Process]
DBUploadRetry = 2

[DB]
UserName = MongoDB Atlasのユーザ名をここに記載(ダブルクオーテーションは不要)
ClusterName = MongoDB Atlasのクラスタ名をここに記載(ダブルクオーテーションは不要)
DBName = MongoDB AtlasのDB名をここに記載(ダブルクオーテーションは不要)
TableName = MongoDB Atlasのコレクション名をここに記載(ダブルクオーテーションは不要)

设备清单.csv

根据所使用的设备,添加 No 列(请根据需要适当更改描述)。

No,DeviceName,SensorType,MacAddress,Timeout,Retry,Offset_Temp,Offset_Humid,API_URL,Token
1,SwitchBot_Thermo1,SwitchBot_Thermo,[SwitchBotのMacアドレス],4,3,0,0,,
2,Inkbird_IBSTH1_Mini1,Inkbird_IBSTH1mini,[IBS-TH1 miniのMacアドレス],0,2,0,0,,
3,Inkbird_IBSTH1_1,Inkbird_IBSTH1,[IBS-TH1のMacアドレス],0,2,0,0,,
4,Remo1,Nature_Remo,,0,2,0,0,https://api.nature.global/,[Nature Remoのアクセストークン]
5,Omron_USB1,Omron_USB_EP,[Omron USB型のMacアドレス],0,2,0,0,,
6,Omron_BAG1,Omron_BAG_EP,[Omron BAG型のMacアドレス],3,2,0,0,,

重新大棋.py

import json
import requests
import glob
import pandas as pd

#Remoデータ取得クラス
class GetRemoData():
    def get_sensor_data(self, Token, API_URL):
        headers = {
            'accept': 'application/json',
            'Authorization': 'Bearer ' + Token,
        }
        response = requests.get(f"{API_URL}/1/devices", headers=headers)
        rjson = response.json()
        return self._decodeSensorData(rjson)

    def get_aircon_power_data(self, Token, API_URL):
        headers = {
            'accept': 'application/json',
            'Authorization': 'Bearer ' + Token,
        }
        response = requests.get(f"{API_URL}/1/appliances", headers=headers)
        rjson = response.json()
        return self._decodeAirconPowerData(rjson)

    def calc_human_motion(self, Human_last, csvdir):
        filelist = glob.glob(f"{csvdir}/*/*.csv")
        if len(filelist) == 0:
            return 0
        filelist.sort()
        df = pd.read_csv(filelist[-1])
        if df.Human_last[len(df) - 1] != Human_last:
            return 1
        else:
            return 0

    # センサデータを取り出してdict形式に変換
    def _decodeSensorData(self, rjson):
        for device in rjson:
            #Remoのデータ
            if device['firmware_version'].split('/')[0] == 'Remo':
                sensorValue = {
                    'SensorType': 'Remo_Sensor',
                    'Temperature': device['newest_events']['te']['val'],
                    'Humidity': device['newest_events']['hu']['val'],
                    'Light': device['newest_events']['il']['val'],
                    'Human_last': device['newest_events']['mo']['created_at']
                }
        return sensorValue

    # エアコンおよび電力データを取り出してdict形式に変換
    def _decodeAirconPowerData(self, rjson):
        Value = {}
        for appliance in rjson:
            #エアコン
            if appliance['type'] == 'AC':
                Value['TempSetting'] = appliance['settings']['temp']
                Value['Mode'] = appliance['settings']['mode']
                Value['AirVolume'] = appliance['settings']['vol']
                Value['AirDirection'] = appliance['settings']['dir']
                Value['Power'] = appliance['settings']['button']
            #スマートメータの電力データ
            elif appliance['type'] == 'EL_SMART_METER':
                for meterValue in appliance['smart_meter']['echonetlite_properties']:
                    if meterValue['name'] == 'normal_direction_cumulative_electric_energy':
                        Value['CumulativeEnergy'] = float(meterValue['val'])/100
                    elif meterValue['name'] == 'measured_instantaneous':
                        Value['Watt'] = int(meterValue['val'])        
        #値を取得できていないとき、Noneとする
        if len(Value) == 0:
            Value = None
        return Value

传感器转换表格.py

我会在主要处理部分和output_mongodb_atlas方法中添加将数据上传到MongoDB Atlas的处理过程。

from bluepy import btle
from omron_env import OmronBroadcastScanDelegate, GetOmronConnectModeData
from inkbird_ibsth1 import GetIBSTH1Data
from switchbot import SwitchbotScanDelegate
from remo import GetRemoData
from mesh import GetMeshFromSpreadsheet
from datetime import datetime, timedelta
import os
import csv
import configparser
import pandas as pd
import requests
import logging
import subprocess
import pymongo
from pit import Pit

#グローバル変数
global masterdate

######オムロン環境センサ(BAG型)の値取得######
def getdata_omron_bag(device):
    #値が得られないとき、最大device.Retry回スキャンを繰り返す
    for i in range(device.Retry):
        #omron_envのセンサ値取得デリゲートを、スキャン時実行に設定
        scanner = btle.Scanner().withDelegate(OmronBroadcastScanDelegate())
        #スキャンしてセンサ値取得
        try:
            scanner.scan(device.Timeout)
        #スキャンでエラーが出たらBluetoothアダプタ再起動
        except:
            restart_hci0(device.DeviceName)
        #値取得できたらループ終了
        if scanner.delegate.sensorValue is not None:
            break
        #値取得できなかったらログに書き込む
        else:
            logging.warning(f'retry to get data [loop{str(i)}, date{str(masterdate)}, device{device.DeviceName}, timeout{device.Timeout}]')
    
    #値取得できていたら、POSTするデータをdictに格納
    if scanner.delegate.sensorValue is not None:
        #POSTするデータ
        data = {        
            'DeviceName': device.DeviceName,        
            'Date_Master': masterdate,
            'Date': datetime.today(),
            'Temperature': scanner.delegate.sensorValue['Temperature'],
            'Humidity': scanner.delegate.sensorValue['Humidity'],
            'Light': scanner.delegate.sensorValue['Light'],
            'UV': scanner.delegate.sensorValue['UV'],
            'Pressure': scanner.delegate.sensorValue['Pressure'],
            'Noise': scanner.delegate.sensorValue['Noise'],
            'BatteryVoltage': scanner.delegate.sensorValue['BatteryVoltage']
        }
        return data
    #値取得できていなかったら、ログ出力してBluetoothアダプタ再起動
    else:
        logging.error(f'cannot get data [date{str(masterdate)}, device{device.DeviceName}, timeout{device.Timeout}]')
        restart_hci0(device.DeviceName)
        return None

######オムロン環境センサ(USB型)のデータ取得######
def getdata_omron_usb(device):
    #値が得られないとき、最大device.Retry回スキャンを繰り返す
    for i in range(device.Retry):
        try:
            sensorValue = GetOmronConnectModeData().get_env_usb_data(device.MacAddress)
        #エラー出たらログ出力
        except:
            logging.warning(f'retry to get data [loop{str(i)}, date{str(masterdate)}, device{device.DeviceName}]')
            sensorValue = None
            continue
        else:
            break
    
    #値取得できていたら、POSTするデータをdictに格納
    if sensorValue is not None:
        #POSTするデータ
        data = {        
            'DeviceName': device.DeviceName,        
            'Date_Master': masterdate,
            'Date': datetime.today(),
            'Temperature': sensorValue['Temperature'],
            'Humidity': sensorValue['Humidity'],
            'Light': sensorValue['Light'],
            'Pressure': sensorValue['Pressure'],
            'Noise': sensorValue['Noise'],
            'eTVOC': sensorValue['eTVOC'],
            'eCO2': sensorValue['eCO2']
        }
        return data
    #値取得できていなかったら、ログ出力してBluetoothアダプタ再起動
    else:
        logging.error(f'cannot get data [loop{str(device.Retry)}, date{str(masterdate)}, device{device.DeviceName}]')
        restart_hci0(device.DeviceName)
        return None

######Inkbird IBS-TH1のデータ取得######
def getdata_ibsth1(device):
    #値が得られないとき、最大device.Retry回スキャンを繰り返す
    for i in range(device.Retry):
        try:
            sensorValue = GetIBSTH1Data().get_ibsth1_data(device.MacAddress, device.SensorType)
        #エラー出たらログ出力
        except:
            logging.warning(f'retry to get data [loop{str(i)}, date{str(masterdate)}, device{device.DeviceName}]')
            sensorValue = None
            continue
        else:
            break

    if sensorValue is not None:
        #POSTするデータ
        data = {        
            'DeviceName': device.DeviceName,        
            'Date_Master': masterdate,
            'Date': datetime.today(),
            'Temperature': sensorValue['Temperature'],
            'Humidity': sensorValue['Humidity']
        }
        return data
    #値取得できていなかったら、ログ出力してBluetoothアダプタ再起動
    else:
        logging.error(f'cannot get data [loop{str(device.Retry)}, date{str(masterdate)}, device{device.DeviceName}]')
        restart_hci0(device.DeviceName)
        return None

######SwitchBot温湿度計のデータ取得######
def getdata_switchbot_thermo(device):
    #値が得られないとき、最大device.Retry回スキャンを繰り返す
    for i in range(device.Retry):
        #switchbotのセンサ値取得デリゲートを設定
        scanner = btle.Scanner().withDelegate(SwitchbotScanDelegate(str.lower(device.MacAddress)))
        #スキャンしてセンサ値取得
        try:
            scanner.scan(device.Timeout)
        #スキャンでエラーが出たらBluetoothアダプタ再起動
        except:
            restart_hci0(device.DeviceName)
        #値取得できたらループ終了
        if scanner.delegate.sensorValue is not None:
            break
        #値取得できなかったらログに書き込む
        else:
            logging.warning(f'retry to get data [loop{str(i)}, date{str(masterdate)}, device{device.DeviceName}, timeout{device.Timeout}]')
    
    #値取得できていたら、POSTするデータをdictに格納
    if scanner.delegate.sensorValue is not None:
        #POSTするデータ
        data = {        
            'DeviceName': device.DeviceName,
            'Date_Master': masterdate,
            'Date': datetime.today(),
            'Temperature': scanner.delegate.sensorValue['Temperature'],
            'Humidity': float(scanner.delegate.sensorValue['Humidity']),
            'BatteryVoltage': scanner.delegate.sensorValue['BatteryVoltage']
        }
        return data
    #取得できていなかったら、ログ出力してBluetoothアダプタ再起動
    else:
        logging.error(f'cannot get data [loop{str(device.Retry)}, date{str(masterdate)}, device{device.DeviceName}, timeout{device.Timeout}]')
        restart_hci0(device.DeviceName)
        return None

######Nature Remoのデータ取得######
def getdata_remo(device, csvpath):
    #センサデータ値が得られないとき、最大device.Retry回スキャンを繰り返す
    for i in range(device.Retry):
        try:
            sensorValue = GetRemoData().get_sensor_data(device.Token, device.API_URL)
        #エラー出たらログ出力
        except:
            logging.warning(f'retry to get data [loop{str(i)}, date{str(masterdate)}, device{device.DeviceName}, sensor]')
            sensorValue = None
            continue
        else:
            break
    #エアコンおよび電力データ値が得られないとき、最大device.Retry回スキャンを繰り返す
    for i in range(device.Retry):
        try:
            airconPowerValue = GetRemoData().get_aircon_power_data(device.Token, device.API_URL)
        #エラー出たらログ出力
        except:
            logging.warning(f'retry to get data [loop{str(i)}, date{str(masterdate)}, device{device.DeviceName}, aircon]')
            sensorValue = None
            continue
        else:
            break
        
    #値取得できていたら、POSTするデータをdictに格納
    if sensorValue is not None:
        #センサデータ
        data = {        
            'DeviceName': device.DeviceName,        
            'Date_Master': masterdate,
            'Date': datetime.today(),
            'Temperature': float(sensorValue['Temperature']),
            'Humidity': float(sensorValue['Humidity']),
            'Light': sensorValue['Light'],
            'Human_last': sensorValue['Human_last'],
            'HumanMotion': GetRemoData().calc_human_motion(sensorValue['Human_last'], f'{csvpath}/{device.DeviceName}')
        }
        #エアコン&電力データ
        if airconPowerValue is not None:
            data['TempSetting'] = int(airconPowerValue['TempSetting'])
            data['AirconMode'] = airconPowerValue['Mode']
            data['AirVolume'] = airconPowerValue['AirVolume']
            data['AirDirection'] = airconPowerValue['AirDirection']
            data['AirconPower'] = airconPowerValue['Power']
            if data['AirconPower'] == "":
                data['AirconPower'] = 'power-on_maybe'
            #電力
            if 'CumulativeEnergy' in airconPowerValue:
                data['CumulativeEnergy'] = float(airconPowerValue['CumulativeEnergy'])
            if 'Watt' in airconPowerValue:
                data['Watt'] = int(airconPowerValue['Watt'])
        return data
    #取得できていなかったら、ログ出力(WiFi経由なのでBluetoothアダプタ再起動はしない)
    else:
        logging.error(f'cannot get data [loop{str(device.Retry)}, date{str(masterdate)}, device{device.DeviceName}]')
        return None

######データのCSV出力######
def output_csv(data, csvpath):
    dvname = data['DeviceName']
    monthstr = masterdate.strftime('%Y%m')
    #出力先フォルダ名
    outdir = f'{csvpath}/{dvname}/{masterdate.year}'
    #出力先フォルダが存在しないとき、新規作成
    os.makedirs(outdir, exist_ok=True)
    #出力ファイルのパス
    outpath = f'{outdir}/{dvname}_{monthstr}.csv'
    
    #出力ファイル存在しないとき、新たに作成
    if not os.path.exists(outpath):        
        with open(outpath, 'w') as f:
            writer = csv.DictWriter(f, data.keys())
            writer.writeheader()
            writer.writerow(data)
    #出力ファイル存在するとき、1行追加
    else:
        with open(outpath, 'a') as f:
            writer = csv.DictWriter(f, data.keys())
            writer.writerow(data)

######MongoDB Atlasにアップロードする処理######
def output_mongodb_atlas(all_values_dict, user_name, cluster_name, db_name, collection_name, retry):
    passwd = ****#適宜パスワード隠蔽処理を作成してください
    for i in range(retry):
        try:
            client = pymongo.MongoClient(f"mongodb+srv://{user_name}:{passwd}@{cluster_name}.jipvx.mongodb.net/{db_name}?retryWrites=true&w=majority")
            db = client[db_name]
            collection = db[collection_name]
            result = collection.insert_one(all_values_dict)
        #エラー出たらログ出力
        except:
            if i == retry:
                logging.error(f'cannot upload to DB [loop{str(i)}, date{str(masterdate)}]')
            else:
                logging.warning(f'retry to upload to DB [loop{str(i)}, date{str(masterdate)}]')
            continue
        else:
            break

######Googleスプレッドシートにアップロードする処理######
def output_spreadsheet(all_values_dict_str):
    #APIのURL
    url = 'GAS APIのURLをここに記載'
    #APIにデータをPOST
    response = requests.post(url, json=all_values_dict_str)
    print(response.text)

######Bluetoothアダプタ再起動######
def restart_hci0(devicename):
    passwd = 'RaspberryPiのパスワードを入力'#適宜隠蔽してください
    subprocess.run(('sudo','-S','hciconfig','hci0','down'), input=passwd, check=True)
    subprocess.run(('sudo','-S','hciconfig','hci0','up'), input=passwd, check=True)
    logging.error(f'restart bluetooth adapter [date{str(masterdate)}, device{devicename}]')


######メイン######
if __name__ == '__main__':
    #開始時刻を取得
    startdate = datetime.today()
    #開始時刻を分単位で丸める
    masterdate = startdate.replace(second=0, microsecond=0)   
    if startdate.second >= 30:
        masterdate += timedelta(minutes=1)

    #設定ファイルとデバイスリスト読込
    cfg = configparser.ConfigParser()
    cfg.read('./config.ini', encoding='utf-8')
    df_devicelist = pd.read_csv('./DeviceList.csv')
    #全センサ数とデータ取得成功数
    sensor_num = len(df_devicelist)
    success_num = 0

    #ログの初期化
    logname = f"/sensorlog_{str(masterdate.strftime('%y%m%d'))}.log"
    logging.basicConfig(filename=cfg['Path']['LogOutput'] + logname, level=logging.INFO)

    #取得した全データ保持用dict
    all_values_dict = None
    #上記dictの文字列バージョン(GAS Post用、datetime型がJSON化できないため)
    all_values_dict_str = None

    #データ取得開始時刻
    scan_start_date = datetime.today()

    ######デバイスごとにデータ取得######
    for device in df_devicelist.itertuples():
        #Omron環境センサBAG型(BroadCast接続)
        if device.SensorType in ['Omron_BAG_EP','Omron_BAG_IM']:
            data = getdata_omron_bag(device)
        #Omron環境センサUSB型(Connectモード接続)
        elif device.SensorType in ['Omron_USB_EP','Omron_USB_IM']:
            data = getdata_omron_usb(device)
        #Inkbird IBS-TH1
        elif device.SensorType in ['Inkbird_IBSTH1mini','Inkbird_IBSTH1']:
            data = getdata_ibsth1(device)
        #SwitchBot温湿度計
        elif device.SensorType == 'SwitchBot_Thermo':
            data = getdata_switchbot_thermo(device)
        #remo
        elif device.SensorType == 'Nature_Remo':
            data = getdata_remo(device, cfg['Path']['CSVOutput'])
        #mesh
        elif device.SensorType == 'Sony_MeshHuman':
            data = getdata_mesh_human(device)
        #上記以外
        else:
            data = None        

        #データが存在するとき、全データ保持用Dictに追加し、CSV出力
        if data is not None:
            #all_values_dictがNoneのとき、新たに辞書を作成
            if all_values_dict is None:
                #all_values_dictを作成(最初なのでDate_MasterとDate_ScanStartも追加)
                all_values_dict = {'Date_Master':data['Date_Master'], 'Date_ScanStart':scan_start_date}
                all_values_dict.update(dict([('no'+format(device.No,'02d')+'_'+k, v) for k,v in data.items() if k != 'Date_Master']))
                #dataを文字列変換してall_values_dict_strを作成(最初なのでDate_ScanStartを追加)
                data_str = dict([(k, str(v)) for k,v in data.items()])
                data_str['Date_ScanStart'] = str(scan_start_date)
                all_values_dict_str = {data_str['DeviceName']: data_str}
            #all_values_dictがNoneでないとき、既存の辞書に追加
            else:
                #all_values_dictに追加(最初でないのでDate_Masterは除外)
                all_values_dict.update(dict([('no'+format(device.No,'02d')+'_'+k, v) for k,v in data.items() if k != 'Date_Master']))
                #all_values_dict_strに追加
                data_str = dict([(k, str(v)) for k,v in data.items()])
                all_values_dict_str[data_str['DeviceName']] = data_str

            #CSV出力
            output_csv(data_str, cfg['Path']['CSVOutput'])
            #成功数プラス
            success_num+=1

    ######MongoDB Atlasにアップロードする処理######
    all_values_dict['_partition'] = 'Project HomeIoT'#あとでMongoDB Realmの同期に使用するフィールド
    output_mongodb_atlas(all_values_dict, cfg['DB']['UserName'], cfg['DB']['ClusterName'], cfg['DB']['DBName'], cfg['DB']['TableName'], int(cfg['Process']['DBUploadRetry']))

    ######Googleスプレッドシートにアップロードする処理######
    output_spreadsheet(all_values_dict_str)

    #処理終了をログ出力
    logging.info(f'[masterdate{str(masterdate)} startdate{str(startdate)} enddate{str(datetime.today())} success{str(success_num)}/{str(sensor_num)}]')

在sensors_to_spreadsheet.py文件中的output_mongodb_atlas()函数中,将名为”all_values_dict”的文档数据POST到MongoDB Atlas。

请确保上述output_mongodb_atlas方法中的处理与前一章中Python对MongoDB Atlas的访问内容相匹配。

2-2. 确认将数据上传到数据库的操作

15_confirm_post.png

3. MongoDB Realm 服务器端初始设置

我們將根據MongoDB Realm的服務器端設定進行操作。我們參考了這裡的內容。

3-1. 创建Realm应用程。

1_select_realm.png
2_start_new_realm_app.png
3_cretate_realm_app.png
04_copy_appid.png

3-2. 制定规则 (zhì guī zé)

05_configure_collection.png

创建模式

请按以下步骤创建模式。

06_generate_schema1.png
07_generate_schema2.png
08_generate_schema3.png
19_schema_sensor_list.png
{
  "title": "SensorList",
  "bsonType": "object",
  "required": ["_id"],
  "properties": {
    "_id": {
      "bsonType": "objectId"
    },
    "_partition": {
      "bsonType": "string"
    },
    "aircon": {
      "bsonType": "bool"
    },
    "humidity": {
      "bsonType": "bool"
    },
    "no": {
      "bsonType": "int"
    },
    "place": {
      "bsonType": "string"
    },
    "power": {
      "bsonType": "bool"
    },
    "sensorname": {
      "bsonType": "string"
    },
    "temperature": {
      "bsonType": "bool"
    }
  }
}

用户权限设置

15_configure_user_authentication2.png

相当于中文的「同期设定」,指的是在同一时间段内所做的设定或安排。

21_add_sync.png

将更改部署

18_deploy_change.png

4. 开发Android应用程序

我将使用MongoDB Realm创建一个可以与服务器和数据同步的Android应用程序。我将使用Android Studio进行开发。在开发过程中,我主要参考了官方文档的基本指南。

创建项目

30_select_template.png
31_configure_project.png

4-2. 安装 Realm 插件

安装必要的插件。

安装项目给全体成员。

32_install_project_gradle.png
buildscript {
      :
    repositories {
          :
        maven {
            url 'http://oss.jfrog.org/artifactory/oss-snapshot-local'
        }
    }
    dependencies {
          :
        classpath "io.realm:realm-gradle-plugin:10.0.0-BETA.5"
    }
allprojects {
    repositories {
          :
        maven {
            url 'http://oss.jfrog.org/artifactory/oss-snapshot-local'
        }
    }
}
  :

安装到应用程序中

33_install_app_gradle.png
  :
apply plugin: 'kotlin-kapt'
apply plugin: 'realm-android'
  :
android {
      :
    buildTypes {
        def appId = "上で控えたアプリケーションID"  // Replace with proper Application ID
        debug {
            buildConfigField "String", "MONGODB_REALM_APP_ID", "\"${appId}\""
        }
        release {
            buildConfigField "String", "MONGODB_REALM_APP_ID", "\"${appId}\""
            minifyEnabled false
            signingConfig signingConfigs.debug
        }
    }

    compileOptions {
        sourceCompatibility 1.8
        targetCompatibility 1.8
    }
}

realm {
    syncEnabled = true
}

dependencies {
      :
    implementation 'com.google.android.material:material:1.2.0'
      :
    implementation "io.realm:android-adapters:4.0.0"
    implementation "androidx.recyclerview:recyclerview:1.1.0"
      :
}
34_run_emulator.png

4-3. 创建数据模型.

我将创建一个 Kotlin 类(数据模型),用于从服务器数据库的集合接收数据。具体来说,我将创建以下两个类。

Sensor.kt:适用于传感器数据上传的集合
SensorList.kt:适用于sensor_lists集合

创建model包

按照以下步骤,创建包含模型的文件夹。

35.png
36.png

传感器.kt

38.png
39.png

将通过上述操作确认的内容复制粘贴到SensorData.kt中。应该如下所示:

package com.mongodb.homeiotviewer.model

import io.realm.RealmObject;
import io.realm.annotations.PrimaryKey
import java.util.Date;
import org.bson.types.ObjectId;

open class Sensor (
    @PrimaryKey var _id: ObjectId = ObjectId(),
    var Date_Master: Date = Date(),
    var Date_ScanStart: Date? = null,
    var _partition: String? = null,
    var no01_Date: Date? = null,
    var no01_DeviceName: String? = null,
    var no01_Humidity: Double? = null,
    var no01_Temperature: Double? = null,
      :
    中略
      :
    var no07_Humidity: Double? = null,
    var no07_Temperature: Double? = null,
    ): RealmObject() {}

传感器列表.kt

按照上述相同的方法,在model文件夹内创建SensorList.kt。

package com.mongodb.homeiotviewer.model

import io.realm.RealmObject;
import io.realm.annotations.PrimaryKey
import org.bson.types.ObjectId;

open class SensorList (
    @PrimaryKey var _id: ObjectId = ObjectId(),
    var _partition: String? = null,
    var aircon: Boolean? = null,
    var humidity: Boolean? = null,
    var no: Long? = null,
    var place: String? = null,
    var power: Boolean? = null,
    var sensorname: String? = null,
    var temperature: Boolean? = null
): RealmObject() {}

创建用于生成全局实例的类。

在整个应用程序中定义共享的全局实例。
具体地说,如下所示

应用程序:Realm应用程序实例(App类)

创建班级

35_make_global_realmapp_instance.png
package com.mongodb.homeiotviewer

import android.app.Application
import android.util.Log

import io.realm.Realm
import io.realm.log.LogLevel
import io.realm.log.RealmLog
import io.realm.mongodb.App
import io.realm.mongodb.AppConfiguration

//Realmアプリケーションのインスタンス(グローバルインスタンスとして、アプリケーション全体で共有する)
lateinit var app: App

// global Kotlin extension that resolves to the short version
// of the name of the current class. Used for labelling logs.
inline fun <reified T> T.TAG(): String = T::class.java.simpleName

/*
* InitRealm: Sets up the Realm App and enables Realm-specific logging in debug mode.
*/
class InitRealm : Application() {

    override fun onCreate() {
        super.onCreate()
        //Realmライブラリの初期化
        Realm.init(this)
        //Realmアプリケーションにアクセスしインスタンス化
        app = App(
            AppConfiguration.Builder(BuildConfig.MONGODB_REALM_APP_ID)
                .build())

        // デバッグモード時に追加ロギングを有効に
        if (BuildConfig.DEBUG) {
            RealmLog.setLevel(LogLevel.ALL)
        }

        Log.v(TAG(), "Initialized the Realm App configuration for: ${app.configuration.appId}")
    }
}

在中文的本地注册引进。

40_add_globalinstance_class_to_manifest.png
    <application
          :
        android:name="com.mongodb.homeiotviewer.InitRealm">
          :

创建登录页面

我将创建一个用于创建或登录 MongoDB Realm 用户账户的界面。

创建登录页面活动

38_make_login_activity2.png

创建登录界面的布局

使用以下操作,对屏幕布局activity_login.xml进行编辑。

39_edit_login_activity_layout.png
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".LoginActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:paddingLeft="24dp"
        android:paddingTop="12dp"
        android:paddingRight="24dp">

        <com.google.android.material.textfield.TextInputLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:layout_marginBottom="8dp">

            <EditText
                android:id="@+id/input_username"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="@string/username"
                android:inputType="text" />
        </com.google.android.material.textfield.TextInputLayout>

        <com.google.android.material.textfield.TextInputLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:layout_marginBottom="8dp">

            <EditText
                android:id="@+id/input_password"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="@string/password"
                android:inputType="textPassword" />
        </com.google.android.material.textfield.TextInputLayout>

        <androidx.appcompat.widget.AppCompatButton
            android:id="@+id/button_login"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="24dp"
            android:layout_marginBottom="12dp"
            android:padding="12dp"
            android:text="@string/login" />

        <androidx.appcompat.widget.AppCompatButton
            android:id="@+id/button_create"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="24dp"
            android:padding="12dp"
            android:text="@string/create_account" />
    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>
40_edit_stringsxml.png
<resources>
    <string name="app_name">RealmTutorial</string>//ここはプロジェクト名が元々記載されている
    <string name="username">Email</string>
    <string name="password">Password</string>
    <string name="create_account">Create account</string>
    <string name="login">Login</string>
    <string name="more">\u22EE</string>
    <string name="new_task">Create new task</string>
    <string name="logout">Log Out</string>
</resources>
41_confirm_login_activity_layout.png

创建从MainActivity到登录页面的跳转。

当没有存在登录中的用户时,将会转到登录页面, 在java -> 包名文件夹中找到MainActivity.kt,然后按照下面的方式进行修改.

package com.mongodb.homeiotviewer//パッケージ名に合わせて修正

import android.content.Intent
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import io.realm.mongodb.User

class MainActivity : AppCompatActivity() {
    private var user: User? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    override fun onStart() {
        super.onStart()
        //ログイン中ユーザの取得
        try {
            user = app.currentUser()
        } catch (e: IllegalStateException) {
            Log.w(TAG(), e)
        }
        //ログイン中ユーザが存在しない時、ログイン画面を表示する
        if (user == null) {
            // if no user is currently logged in, start the login activity so the user can authenticate
            startActivity(Intent(this, LoginActivity::class.java))
        }
    }
}

创建登录处理类。

在登录页面按下按钮时的处理将实现如下。

将位于包名文件夹内的LoginActivity.kt按照以下方式进行修改

package com.mongodb.homeiotviewer

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.EditText
import android.widget.Toast
import io.realm.mongodb.Credentials

class LoginActivity : AppCompatActivity() {
    //各種入力フォームを保持するインスタンス
    private lateinit var username: EditText//ユーザ名(Eメールアドレス)入力用テキストボックス
    private lateinit var password: EditText//パスワード入力用テキストボックス
    private lateinit var loginButton: Button//ログインボタン
    private lateinit var createUserButton: Button//新規ユーザ作成ボタン

    public override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //入力フォームインスタンスの生成
        setContentView(R.layout.activity_login)
        username = findViewById(R.id.input_username)
        password = findViewById(R.id.input_password)
        loginButton = findViewById(R.id.button_login)
        createUserButton = findViewById(R.id.button_create)
        //ボタンを押したときの処理
        loginButton.setOnClickListener { login(false) }//ログインホタン
        createUserButton.setOnClickListener { login(true) }//新規ユーザ作成ボタン
    }

    override fun onBackPressed() {
        //戻るボタンでメイン画面に戻れないようにする(メイン画面に戻るにはログインが必須)
        // Disable going back to the MainActivity
        moveTaskToBack(true)
    }

    private fun onLoginSuccess() {
        //ログインに成功したら、メイン画面に戻る
        // successful login ends this activity, bringing the user back to the main activity
        finish()
    }

    private fun onLoginFailed(errorMsg: String) {
        //ログインに失敗したら、ログに書き込んだ上でメッセージ表示
        Log.e(TAG(), errorMsg)
        Toast.makeText(baseContext, errorMsg, Toast.LENGTH_LONG).show()
    }

    private fun validateCredentials(): Boolean = when {
        //ユーザ名とパスワードが空欄でないことを確認
        // zero-length usernames and passwords are not valid (or secure), so prevent users from creating accounts with those client-side.
        username.text.toString().isEmpty() -> false
        password.text.toString().isEmpty() -> false
        else -> true
    }

    /**
     * ログインボタンを押したときの処理
     * @param[createUser]:trueなら新規ユーザ作成、falseなら通常のログイン
     */
    // handle user authentication (login) and account creation
    private fun login(createUser: Boolean) {
        if (!validateCredentials()) {
            onLoginFailed("Invalid username or password")
            return
        }

        //処理中はボタンを押せないようにする
        // while this operation completes, disable the buttons to login or create a new account
        createUserButton.isEnabled = false
        loginButton.isEnabled = false

        val username = this.username.text.toString()
        val password = this.password.text.toString()

        if (createUser) {//新規ユーザ作成のとき
            // ユーザ名(E-mailアドレス)+パスワードでユーザ作成実行
            // register a user using the Realm App we created in the TaskTracker class
            app.emailPasswordAuth.registerUserAsync(username, password) {
                // re-enable the buttons after user registration completes
                createUserButton.isEnabled = true
                loginButton.isEnabled = true
                if (!it.isSuccess) {//ユーザ作成失敗時は、メッセージを表示
                    onLoginFailed("Could not register user.")
                    Log.e(TAG(), "Error: ${it.error}")
                } else {//成功時は、そのまま通常ログイン
                    Log.i(TAG(), "Successfully registered user.")
                    // when the account has been created successfully, log in to the account
                    login(false)
                }
            }
        } else {//通常ログインのとき
            val creds = Credentials.emailPassword(username, password)
            app.loginAsync(creds) {
                // re-enable the buttons after
                loginButton.isEnabled = true
                createUserButton.isEnabled = true
                if (!it.isSuccess) {//ログイン失敗時は、メッセージを表示
                    onLoginFailed(it.error.message ?: "An error occurred.")
                } else {//成功時は、メイン画面に戻る
                    onLoginSuccess()
                }
            }
        }
    }
}

基本的处理流程如下:
· 登录按钮:确认输入用户名和密码 → 登录 → 如果成功,则转到主界面
· 创建新用户按钮:确认输入用户名和密码 → 创建用户 → 如果成功,则执行登录按钮的处理

4-6. 创建登出处理

添加登出菜单

通过以下步骤,在logout_menu.xml文件中添加注销菜单布局。

41_make_option_menu_layout4.png

将生成的logout_menu.xml文件按照以下方式进行修改:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context=".CounterActivity">
    <item
        android:id="@+id/action_logout"
        android:orderInCategory="100"
        android:title="@string/logout"
        android:text="@string/logout"
        app:showAsAction="always"/>
</menu>

在MainActivity中添加注销操作。

为了将创建的登出菜单添加到主活动中,
将MainActivity.kt按照以下方式进行改写。

package com.mongodb.homeiotviewer

import android.content.Intent
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import io.realm.Realm
import io.realm.mongodb.User
import io.realm.mongodb.sync.SyncConfiguration

class MainActivity : AppCompatActivity() {
    private lateinit var realm: Realm//Realmデータベースのインスタンス
    private var user: User? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //Realmデータベースのインスタンス初期化
        realm = Realm.getDefaultInstance()
    }

    override fun onStart() {
        super.onStart()
        //ログイン中ユーザの取得
        try {
            user = app.currentUser()
        } catch (e: IllegalStateException) {
            Log.w(TAG(), e)
        }
        //ログイン中ユーザが存在しない時、ログイン画面を表示する
        if (user == null) {
            // if no user is currently logged in, start the login activity so the user can authenticate
            startActivity(Intent(this, LoginActivity::class.java))
        }
    }

    override fun onStop() {
        super.onStop()
        user.run {
            realm.close()
        }
    }

    //アクティビティ終了時の処理(realmインスタンスをClose)
    override fun onDestroy() {
        super.onDestroy()
        // if a user hasn't logged out when the activity exits, still need to explicitly close the realm
        realm.close()
    }

    //logoutメニューをMainActivity上に設置
    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        menuInflater.inflate(R.menu.logout_menu, menu)
        return true
    }

    //logoutメニューを押したときの処理(ログアウト)
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when (item.itemId) {
            R.id.action_logout -> {
                user?.logOutAsync {
                    if (it.isSuccess) {
                        // always close the realm when finished interacting to free up resources
                        realm.close()
                        user = null
                        Log.v(TAG(), "user logged out")
                        startActivity(Intent(this, LoginActivity::class.java))
                    } else {
                        Log.e(TAG(), "log out failed! Error: ${it.error}")
                    }
                }
                true
            }
            else -> {
                super.onOptionsItemSelected(item)
            }
        }
    }
}

登录和登出操作确认

在这个阶段,通过模拟器执行软件并确认登录和退出登录过程是否正常(通常情况下,您可以按照本文中的方法进行登录和退出登录)。

4-7. 获取和显示数据

同步和获取Realm领域的数据,以及展示所获取的数据。

准备使用布局模式。

将默认显示”hello world”的TextView,重用于从Realm获取的数据显示。

在activity_main.xml中添加android:id=”@+id/query_test”的声明(以便可以通过代码进行访问)。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/query_test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

添加同期和查询处理

在MainActivity.kt中进行以下操作:
– 实例化上一节中创建的TextView,并添加能够追加文字的功能。
– 进行同步和查询以获取数据的处理。

TextView相关的处理如下:

      :
    private lateinit var queryTestView: TextView//クエリ結果表示用のtextViewインスタンス
      :
    override fun onCreate(savedInstanceState: Bundle?) {
          :
        //クエリ結果表示用のtextViewインスタンス作成
        queryTestView = findViewById(R.id.query_test)

根据当前时间和查询,数据获取的处理方式如下所示。

      :
    override fun onStart() {
          :
        //ログイン中ユーザが存在しないとき
        if (user == null) {
              :
        }
        //ログイン中ユーザが存在するとき
        else {
            //MongoDB Realmとの同期設定
            val partitionValue: String = "Project HomeIoT"//
            val config = SyncConfiguration.Builder(user!!, "Project HomeIoT")
                .waitForInitialRemoteData()
                .build()
            //上記設定をデフォルトとして保存
            Realm.setDefaultConfiguration(config)
            //非同期バックグラウンド処理でMongoDB Realmと同期実行
            Realm.getInstanceAsync(config, object: Realm.Callback() {
                override fun onSuccess(realm: Realm) {
                    //同期したRealmインスタンスを親クラスMainActivityのインスタンスに設定
                    this@MainActivity.realm = realm
                    //クエリ操作用インスタンス作成
                    val listQuery = realm.where<SensorList>()
                    ////////以下の処理は、取得するデータ内容に応じて変える////////
                    val result = listQuery.sort("no").findAll()
                    var resultString: String = ""
                    for(device in result){
                        resultString += "${device.no},${device.sensorname},${device.place}\n"
                    }
                    //クエリ結果表示用のtextViewインスタンス
                    queryTestView.text = resultString
                }
            })
        }
    }

根据公式文档看来,如果想要在数据同步的同时获取和更新内容,就需要在下面的回调处理中编写代码,这是非常重要的。(其中还包括片段适配器的处理)

    Realm.getInstanceAsync(config, object: Realm.Callback() {
        override fun onSuccess(realm: Realm) {
            //この中にデータの取得、更新関係の処理を全て記載
        }
    }

应用了上述的修改之后,MainActivity.kt应该会变成以下这样。

package com.mongodb.homeiotviewer

import android.content.Intent
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.TextView
import com.mongodb.homeiotviewer.model.Sensor
import com.mongodb.homeiotviewer.model.SensorList
import io.realm.Realm
import io.realm.mongodb.User
import io.realm.mongodb.sync.SyncConfiguration
import io.realm.kotlin.where

class MainActivity : AppCompatActivity() {
    private lateinit var realm: Realm//Realmデータベースのインスタンス
    private var user: User? = null
    private lateinit var queryTestView: TextView//クエリ結果表示用のtextViewインスタンス

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //Realmデータベースのインスタンス初期化
        realm = Realm.getDefaultInstance()

        //クエリ結果表示用のtextViewインスタンス作成
        queryTestView = findViewById(R.id.query_test)
    }

    override fun onStart() {
        super.onStart()
        //ログイン中ユーザの取得
        try {
            user = app.currentUser()
        } catch (e: IllegalStateException) {
            Log.w(TAG(), e)
        }
        //ログイン中ユーザが存在しない時、ログイン画面を表示する
        if (user == null) {
            // if no user is currently logged in, start the login activity so the user can authenticate
            startActivity(Intent(this, LoginActivity::class.java))
        }
        //ログイン中ユーザが存在するとき
        else {
            //MongoDB Realmとの同期設定
            val partitionValue: String = "Project HomeIoT"//パーティションの名前
            val config = SyncConfiguration.Builder(user!!, "Project HomeIoT")
                .waitForInitialRemoteData()
                .build()
            //上記設定をデフォルトとして保存
            Realm.setDefaultConfiguration(config)
            //非同期バックグラウンド処理でMongoDB Realmと同期実行
            Realm.getInstanceAsync(config, object: Realm.Callback() {
                override fun onSuccess(realm: Realm) {
                    //同期したRealmインスタンスを親クラスMainActivityのインスタンスに設定
                    this@MainActivity.realm = realm
                    //クエリ操作用インスタンス作成
                    val listQuery = realm.where<SensorList>()//sensor_listsコレクション
                    val sensorQuery = realm.where<Sensor>()//sensorsコレクション
                    ////////以下の処理は、取得するデータ内容に応じて変える////////
                    val result = listQuery.sort("no").findAll()
                    var resultString: String = ""
                    for(device in result){
                        resultString += "${device.no},${device.sensorname},${device.place}\n"
                    }
                    //クエリ結果をtextViewに表示
                    queryTestView.text = resultString
                }
            })
        }
    }

    override fun onStop() {
        super.onStop()
        user.run {
            realm.close()
        }
    }

    //アクティビティ終了時の処理(realmインスタンスをClose)
    override fun onDestroy() {
        super.onDestroy()
        // if a user hasn't logged out when the activity exits, still need to explicitly close the realm
        realm.close()
    }

    //logoutメニューをMainActivity上に設置
    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        menuInflater.inflate(R.menu.logout_menu, menu)
        return true
    }

    //logoutメニューを押したときの処理(ログアウト)
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when (item.itemId) {
            R.id.action_logout -> {
                user?.logOutAsync {
                    if (it.isSuccess) {
                        // always close the realm when finished interacting to free up resources
                        realm.close()
                        user = null
                        Log.v(TAG(), "user logged out")
                        startActivity(Intent(this, LoginActivity::class.java))
                    } else {
                        Log.e(TAG(), "log out failed! Error: ${it.error}")
                    }
                }
                true
            }
            else -> {
                super.onOptionsItemSelected(item)
            }
        }
    }
}
50.png
    ////////以下の処理は、取得するデータ内容に応じて変える////////
    val result = listQuery.sort("no").findAll()
    var resultString: String = ""
    for(device in result){
        resultString += "${device.no},${device.sensorname},${device.place}\n"
    }

5. 查询操作

我們將改變前一章節中所提到的查詢處理內容,並嘗試獲取哪些數據。

将排序顺序改为降序

在排序参数中指定Sort.DESCENDING

    val result = listQuery.sort("no", Sort.DESCENDING).findAll()
    var resultString: String = ""
    for(device in result){
        resultString += "${device.no},${device.sensorname},${device.place}\n"
    }
sort.png

过滤处理

同样的

可以使用.equalTo()方法
※也可以使用.notEqualTo()方法来表示不一致

    val result = listQuery.equalTo("place", "indoor").sort("no").findAll()
    var resultString: String = ""
    for(device in result){
        resultString += "${device.no},${device.sensorname},${device.place}\n"
    }
equalto.png

不等号的意思是两个事物不相等。

可以使用greaterThan()或者lessThan()。
还可以使用含等号的.greaterThanOrEqualTo()或者.lessThanOrEqualTo()。

    val result = listQuery.greaterThan("no", 4).sort("no").findAll()
    var resultString: String = ""
    for(device in result){
        resultString += "${device.no},${device.sensorname},${device.place}\n"
    }
greater.png

范围

使用.between()函数

    val result = listQuery.between("no", 2, 6).sort("no").findAll()
    var resultString: String = ""
    for(device in result){
        resultString += "${device.no},${device.sensorname},${device.place}\n"
    }
between.png

指定数组

使用.in()函数

    val result = listQuery.`in`("place", arrayOf("kitchen","outdoor")).sort("no").findAll()
    var resultString: String = ""
    for(device in result){
        resultString += "${device.no},${device.sensorname},${device.place}\n"
    }
in.png

文本处理

请参考官方文档了解如何使用 .beginsWith()、.contains()、.endsWith()、.like() 中的通配符。

    val result = listQuery.beginsWith("sensorname", "Omron").sort("no").findAll()
    var resultString: String = ""
    for(device in result){
        resultString += "${device.no},${device.sensorname},${device.place}\n"
    }
beginswith.png

逻辑推理

使用 .and()、.not()、.or()

    val result = listQuery.beginsWith("sensorname", "Inkbird").and().equalTo("place","indoor").sort("no").findAll()
    var resultString: String = ""
    for(device in result){
        resultString += "${device.no},${device.sensorname},${device.place}\n"
    }
image.png

统计数据

从现在开始使用传感器集合的数据。

平均数和总数

使用.average()和.sum()

    val result = sensorQuery.average("no01_Temperature")
    val resultString = result.toString()
average.png

最大的,最小的

使用 .max() 和 .min() 方法

    val result = sensorQuery.min("no01_Temperature")
    val resultString = result.toString()
min.png

使用.count()的方法

    val result = sensorQuery.isNull("no03_Temperature").count()
    val resultString = result.toString()
nullcount.png

1. 要素获取

第一个元素

您可以使用.findFirst()来获取排序后的第一个元素。

    val result = sensorQuery.sort("Date_Master").findFirst()
    val resultString = "${result?.Date_Master}\n" +
        "${result?.no01_Temperature}゜C\n"+
        "${result?.no01_Humidity}%\n"
first.png

最后一个元素

您可以与Sort.DESCENDING组合,以获取排序后的最后一个元素。

    val result = sensorQuery.sort("Date_Master", Sort.DESCENDING).findFirst()
    val resultString = "${result?.Date_Master}\n" +
        "${result?.no01_Temperature}゜C\n"+
        "${result?.no01_Humidity}%\n"
last.png

除了Read之外的查询

本次只介绍了与读取相关的查询,还有与写入和更新相关的查询。
请参考下面的官方文档:
https://docs.mongodb.com/realm/android/

故障排除

我沉迷于以下的部分是原因。

「title」和数据模型类的命名一致。

我在这个部分浪费了最多时间,也是我最沉迷的地方。

在MongoDB Realm中,当同步云上的数据库与手机应用中的Kotlin数据模型时,似乎是通过匹配云上数据库的”title”架构和Kotlin数据模型的类名称来进行同步的。

这里的麻烦之处在于双方命名规则的差异。
云上的数据库:按照数据库表的命名规则,使用 snake_case(小写下划线分隔)。
Kotlin数据模型:按照 Kotlin 类的命名规则,使用 PascalCase(首字母大写)。
这是常见的做法。

开始的时候,我以sensorlist集合为例

クラウド上のDBコレクション名: “sensor_lists”スキーマのtitle: “sensor_lists”Kotlinデータモデル
クラス名”SensorList”

我之前设置了这样的配置,但是由于架构的标题和Kotlin类名不匹配,所以无法同步。####解决方法是,经过一番调查,我发现这两者的名称匹配是同步的条件。

クラウド上のDBコレクション名: “sensor_lists”スキーマのtitle: “SensorList”Kotlinデータモデル
クラス名”SensorList”

通过进行类似的更改后,现在可以正常进行同步了。

由于形态的变化而导致的同步失败

MongoDB有一个非常方便的功能,可以自动判断类型并进行适配,当进行Post操作时。但是,有时候在定义模式时,类型会意外地发生变化,导致同步失败。

在我的案例中,”no01_Temperature”字段(Nature Remo的温度)在定义模式时被识别为30.4,类型被MongoDB判定为双精度浮点型。然而,在某个时间点,测量温度变为整数30,导致MongoDB将其判定为整数型,与模式类型不符,导致同步失败。

对策

在发布之前,我修改了代码,将其转换为Python中的float数据类型。

'Temperature': sensorValue['Temperature'],
'Temperature': float(sensorValue['Temperature']),

此外,您可以在MongoDB Realm云端界面的Rules选项卡中,选择目标集合,然后进入Schema选项卡,点击VALIDATE按钮,以确认是否存在与此类架构不同的数据。

最后

以上で、后端处理的创建已经完成。

一方面,若要将其作为实用的智能手机应用程序,就需要实现前端处理并创建用户界面。
由于前端部分超出了本文的范围,我们将在下文中再次回到该文章并进行解释。

 

广告
将在 10 秒后关闭
bannerAds