0

Problem
I've been attempting to create a sync between my local database and a MySQL database server using WAMP. This sync sends all data from my database in JSON format and inserts or updates it based on if there is a duplicate key. Afterwards, I retrieve all the data from that MySQL database, clear the local database created by the application and store all the new data in it. However, this is not working as I hoped because the data I send over to the database server keeps overwriting columns which have different keys.

Example in Words
I have a Team with the key 5024 synced between both databases. I add a new Team with the key 3756 to my local database then I try to sync the local database with the MySQL database. However, the sync doesn't go as planned because the database in most cases doesn't insert a new column with the key. It instead updates a row which has a completely DIFFERENT key with the data sent to it.

Extra Information
Sometimes the sync does work as planned and does add a new column with the sent information but this data get overwritten in the next sync

Example JSON Object {"TeamNumber":5024,"TeamName":"Ramferno","AutoFuelLow":"yes","AutoFuelHigh":"no","AutoFuelPoints":15,"AutoRotorEngaged":"no","TeleFuelLow":"yes","TeleFuelHigh":"no","TeleFuelPoints":20,"TeleRotorEngaged":"yes","Hang":"yes","PlayStyle":"Defensive"}

WAMP Server Services & Languages
MySQL: 5.7.14
PHP: 5.6.25
Apache: 2.4.23
PHPMyAdmin: 4.6.6

How Data is Delivered
My android class retrieves data from the local SQLite database stores it in a Cursorobject. It will then go through each row of data in the Cursor and transfer it to a JSONObject which is then sent to the database server through a BufferedWriter which has access to the output stream as long as there is a connection (which has been made on every sync). A PHP script on the database (which was connected to in the android class) gets all information, decodes it through $_POST then uses sends a INSERT INTO ON DUPLICATE KEY SQL query to the database server containing the data received.

Things I've tried

  • Adding a delay
  • Using a JSONArray to only write to the database once
  • Dropping and re-adding key in table as primary or unique

Code
DatabaseTransfer.java (Android class that sends data from local database)

package org.ramferno.ramfernoscouting2017.sql;

import android.app.ProgressDialog;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.os.AsyncTask;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.widget.Toast;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.ramferno.ramfernoscouting2017.LaunchActivity;
import org.ramferno.ramfernoscouting2017.fragments.ScoutFragment;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Iterator;

// Start of DatabaseTransfer
@SuppressWarnings({"FieldCanBeLocal", "ConstantConditions"})
public class DatabaseTransfer extends AsyncTask<String, String, String> {
    // Declare object references
    private Context context;
    private ProgressDialog pDialog;
    private Fragment fragment;

    // Declare variables
    private String urlAddress;

    /**
     * Constructor for the class
     * @param context is the application context
     * @param urlAddress is a url address with an accessible MySQL database
     */
    public DatabaseTransfer(Context context, String urlAddress, Fragment fragment) {
        this.context = context;
        this.urlAddress = urlAddress;
        this.fragment = fragment;
    } //End of constructor

    @Override
    protected void onPreExecute() {
        super.onPreExecute();

        // Display dialog
        pDialog = new ProgressDialog(context);
        pDialog.setMessage("Syncing Data ...");
        pDialog.setIndeterminate(false);
        pDialog.setCancelable(true);
        pDialog.show();
    } // End of method

    @Override
    protected String doInBackground(String... args) {
        sendData();
        getAndStoreData();
        return "true";
    } // End of method

    @Override
    protected void onPostExecute(String s) {
        super.onPostExecute(s);

        ScoutFragment scoutFragment = (ScoutFragment) fragment;
        scoutFragment.refreshListView();

        // Make toast which indicates if response was successful
        Toast.makeText(context, s, Toast.LENGTH_LONG).show();

        // Dismiss dialog
        pDialog.dismiss();
    } // End of method

    /**
     * Attempts a GET connection to a database server
     * @return a connection object
     */
    private HttpURLConnection attemptGetConnection() {
        try {
            // Declare and instantiate URL object
            URL url = new URL("http://" + urlAddress + "/ramfernoscout/database/retrieve.php");

            // Open connection
            HttpURLConnection con = (HttpURLConnection) url.openConnection();

            // Set request properties
            con.setRequestMethod("GET");
            con.setConnectTimeout(20000);
            con.setReadTimeout(20000);
            con.setDoInput(true);

            // Return connection
            return con;
        }
        catch (SocketTimeoutException errTO) {
            Log.e("ERROR TO", errTO.getMessage());
            return null;
        }
        catch (MalformedURLException errURL) {
            Log.e("ERROR URL", errURL.getMessage());
            return null;
        }
        catch (IOException errIO) {
            errIO.printStackTrace();
            return null;
        } // End of try statement
    } // End of method

    /**
     * Attempts a POST connection to a database server
     * @return a connection object
     */
    private HttpURLConnection attemptPostConnection() {
        try {
            URL url = new URL("http://" + urlAddress + "/ramfernoscout/database/updateadd.php");
            HttpURLConnection con = (HttpURLConnection) url.openConnection();

            // Set request properties
            con.setRequestMethod("POST");
            con.setConnectTimeout(20000);
            con.setReadTimeout(20000);
            con.setDoInput(true);
            con.setDoOutput(true);

            // Return connection
            return con;
        }
        catch (SocketTimeoutException errTO) {
            Log.e("ERROR TO", errTO.getMessage());
            return null;
        }
        catch (MalformedURLException errURL) {
            Log.e("ERROR URL", errURL.getMessage());
            return null;
        }
        catch (IOException errIO) {
            errIO.printStackTrace();
            return null;
        } // End of try statement
    } // End of method

    /**
     * Sends all data inside local database to database server
     * @return a response code as a string
     */
    @SuppressWarnings("static-access")
    private String sendData() {
        //Connect the server
        HttpURLConnection con = attemptPostConnection();

        //Check if connection was successful
        if(con == null) {
            return null;
        } //End of if statement

        // Attempt to insert data into the database
        try {
            // Declare and instantiate objects
            OutputStream os = con.getOutputStream();
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os,"UTF-8"));
            final Cursor cursor = LaunchActivity.dbHelper.getData(LaunchActivity.dbHelper
                    .getReadableDatabase(), "*", 0);
            cursor.moveToFirst();

            // Iterate through the entire cursor and add data to database
            for (int i = 0; i < cursor.getCount(); i++) {
                // Write data to database server
                bw.write(packData(cursor));
                cursor.moveToNext();
            } // End of for loop

            // Close stream and writer
            bw.close();
            os.close();

            // Store response code in integer
            int responseCode = con.getResponseCode();

            // Check if data was successfully enter into the database
            if(responseCode == con.HTTP_OK) {
                // Declare and instantiate objects
                BufferedReader br = new BufferedReader(new InputStreamReader
                        (con.getInputStream()));
                StringBuilder response = new StringBuilder();
                String line;

                // Get full response
                while ((line=br.readLine()) != null) {
                    response.append(line);
                } // End of while statement

                // Close reader and return response
                br.close();
                return response.toString();
            } // End of if statement
        }
        catch (IOException errIO) {
            Log.e("ERROR IO", errIO.getMessage());
            errIO.printStackTrace();
        } //End of try statement
        return null;
    } // End of method

    /**
     * Packs database information into a string which can be sent to the database server
     * @param cursor is a Cursor object containing data
     * @return a string
     */
    private String packData(Cursor cursor) {
        //Declare and instantiate objects
        JSONObject jo = new JSONObject();
        StringBuilder packedData = new StringBuilder();

        // Attempt to send data to the server via JSON
        try {
            // Put all data in a json object
            jo.put(DatabaseContract.ScoutTable.TEAM_NUMBER, cursor.getInt(cursor
                    .getColumnIndexOrThrow(DatabaseContract.ScoutTable.TEAM_NUMBER)));
            jo.put(DatabaseContract.ScoutTable.TEAM_NAME, cursor.getString(cursor
                    .getColumnIndexOrThrow(DatabaseContract.ScoutTable.TEAM_NAME)));
            jo.put(DatabaseContract.ScoutTable.AUTO_FUEL_LOW, cursor.getString(cursor
                    .getColumnIndexOrThrow(DatabaseContract.ScoutTable.AUTO_FUEL_LOW)));
            jo.put(DatabaseContract.ScoutTable.AUTO_FUEL_HIGH, cursor.getString(cursor
                    .getColumnIndexOrThrow(DatabaseContract.ScoutTable.AUTO_FUEL_HIGH)));
            jo.put(DatabaseContract.ScoutTable.AUTO_FUEL_POINTS, cursor.getInt(cursor
                    .getColumnIndexOrThrow(DatabaseContract.ScoutTable.AUTO_FUEL_POINTS)));
            jo.put(DatabaseContract.ScoutTable.AUTO_ROTOR_ENGAGED, cursor.getString(cursor
                    .getColumnIndexOrThrow(DatabaseContract.ScoutTable.AUTO_ROTOR_ENGAGED)));
            jo.put(DatabaseContract.ScoutTable.TELE_FUEL_LOW, cursor.getString(cursor
                    .getColumnIndexOrThrow(DatabaseContract.ScoutTable.TELE_FUEL_LOW)));
            jo.put(DatabaseContract.ScoutTable.TELE_FUEL_HIGH, cursor.getString(cursor
                    .getColumnIndexOrThrow(DatabaseContract.ScoutTable.TELE_FUEL_HIGH)));
            jo.put(DatabaseContract.ScoutTable.TELE_FUEL_POINTS, cursor.getInt(cursor
                    .getColumnIndexOrThrow(DatabaseContract.ScoutTable.TELE_FUEL_POINTS)));
            jo.put(DatabaseContract.ScoutTable.TELE_ROTOR_ENGAGED, cursor.getString(cursor
                    .getColumnIndexOrThrow(DatabaseContract.ScoutTable.TELE_ROTOR_ENGAGED)));
            jo.put(DatabaseContract.ScoutTable.ENDGAME_HANG, cursor.getString(cursor
                    .getColumnIndexOrThrow(DatabaseContract.ScoutTable.ENDGAME_HANG)));
            jo.put(DatabaseContract.ScoutTable.PLAY_STYLE, cursor.getString(cursor
                    .getColumnIndexOrThrow(DatabaseContract.ScoutTable.PLAY_STYLE)));

            Log.e("Print JSON Object", jo.toString());

            // Declare and instantiate objects
            Boolean firstValue = true;
            Iterator it = jo.keys();

            // Organize data in JSON format
            do {
                // Go through each key and value in JSONObject
                String key = it.next().toString();
                String value = jo.get(key).toString();

                // Check if first value in the list
                if(firstValue) {
                    firstValue = false;
                }
                else {
                    packedData.append("&");
                } // End of if statement

                // Append to string in order to create JSON format
                packedData.append(URLEncoder.encode(key,"UTF-8"));
                packedData.append("=");
                packedData.append(URLEncoder.encode(value,"UTF-8"));
            } while (it.hasNext()); // End of do statement

            // Return packed data in string format
            return packedData.toString();
        }
        catch (JSONException errJSON) {
            // If occurs, log error
            Log.e("ERROR JSON", errJSON.getMessage());
        }
        catch (UnsupportedEncodingException errUEE) {
            // If occurs, log error
            Log.e("ERROR UEE", errUEE.getMessage());
        } // End of try statement

        // Return nothing
        return null;
    } // End of packData

    /**
     * Downloads data from the database server and stores it in local database
     */
    private String getAndStoreData() {
        // Attempt connection to server
        HttpURLConnection con = attemptGetConnection();

        // If no connection, return nothing
        if(con == null) {
            return "ERROR: Connection unsuccessful, please make sure you are properly connected " +
                    "to the right IP address which has access to the database server";
        } // End of if statement

        // Declare InputStream
        InputStream is = null;

        // Attempt to read the file of data
        try {
            // Declare and instantiate objects
            is = new BufferedInputStream(con.getInputStream());
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            StringBuilder response = new StringBuilder();

            // Declare object references
            String line;
            String responseString = "";

            // Read all lines of data until the end of the file is reached
            if(br != null) {
                while ((line = br.readLine()) != null) {
                    responseString += line;
                } //End of while statement

                // Add string to string builder
                response.append(responseString);

                // Close reader
                br.close();
            }
            else {
                throw new IOException();
            } //End of if statement

            // Declare and instantiate objects
            JSONArray jsonArray = new JSONArray(response.toString());
            JSONObject jsonObject;

            // Reset scout table
            LaunchActivity.dbHelper.onUpgrade(LaunchActivity.dbHelper.getWritableDatabase(), 0);

            // Iterate through JSON array and add each set to database
            for(int i = 0; i < jsonArray.length(); i++) {
                jsonObject = jsonArray.getJSONObject(i);

                // Store data in Content values
                ContentValues values = new ContentValues();
                values.put(DatabaseContract.ScoutTable.TEAM_NUMBER,
                        jsonObject.getInt(DatabaseContract.ScoutTable.TEAM_NUMBER));
                values.put(DatabaseContract.ScoutTable.TEAM_NAME,
                        jsonObject.getString(DatabaseContract.ScoutTable.TEAM_NAME));
                values.put(DatabaseContract.ScoutTable.AUTO_FUEL_LOW,
                        jsonObject.getString(DatabaseContract.ScoutTable.AUTO_FUEL_LOW));
                values.put(DatabaseContract.ScoutTable.AUTO_FUEL_HIGH,
                        jsonObject.getString(DatabaseContract.ScoutTable.AUTO_FUEL_HIGH));
                values.put(DatabaseContract.ScoutTable.AUTO_FUEL_POINTS,
                        jsonObject.getInt(DatabaseContract.ScoutTable.AUTO_FUEL_POINTS));
                values.put(DatabaseContract.ScoutTable.AUTO_ROTOR_ENGAGED,
                        jsonObject.getString(DatabaseContract.ScoutTable.AUTO_ROTOR_ENGAGED));
                values.put(DatabaseContract.ScoutTable.TELE_FUEL_LOW,
                        jsonObject.getString(DatabaseContract.ScoutTable.TELE_FUEL_LOW));
                values.put(DatabaseContract.ScoutTable.TELE_FUEL_HIGH,
                        jsonObject.getString(DatabaseContract.ScoutTable.TELE_FUEL_HIGH));
                values.put(DatabaseContract.ScoutTable.TELE_FUEL_POINTS,
                        jsonObject.getInt(DatabaseContract.ScoutTable.TELE_FUEL_POINTS));
                values.put(DatabaseContract.ScoutTable.TELE_ROTOR_ENGAGED,
                        jsonObject.getString(DatabaseContract.ScoutTable.TELE_ROTOR_ENGAGED));
                values.put(DatabaseContract.ScoutTable.ENDGAME_HANG,
                        jsonObject.getString(DatabaseContract.ScoutTable.ENDGAME_HANG));
                values.put(DatabaseContract.ScoutTable.PLAY_STYLE,
                        jsonObject.getString(DatabaseContract.ScoutTable.PLAY_STYLE));

                // Add data to database
                LaunchActivity.dbHelper.insert(LaunchActivity.dbHelper.getWritableDatabase(),
                        values, 0);
            } // End of for loop
        }
        catch (IOException errIO) {
            // If occurs, log error and return false
            Log.e("ERROR IO", errIO.getMessage());
            return "ERROR";
        }
        catch (JSONException errJSON) {
            // If occurs, log error and return false
            Log.e("ERROR JSON", errJSON.getMessage());
            return "ERROR";
        }
        finally {
            // Close input stream
            if(is != null) {
                try {
                    is.close();
                }
                catch (IOException e) {
                    e.printStackTrace();
                } // End of try statement
            } // End of if statement
        } // End of try statement

        // Return success message if no error occurs
        return "Sync successful!";
    } // End of method
} // End of class

updateadd.php (What receives the data and sends query to database)

<?php
/*
 * This script will add data into a database
 */

// Import the script that connects to the required database
require "init.php";

// Declare and initialize parameters (These parameters will have data entered into them by the android app)
$teamNumber = $_POST['TeamNumber'];
$teamName = $_POST['TeamName'];
$autoFuelLow = $_POST['AutoFuelLow'];
$autoFuelHigh = $_POST['AutoFuelHigh'];
$autoFuelPoints = $_POST['AutoFuelPoints'];
$autoRotorEngaged = $_POST['AutoRotorEngaged'];
$teleFuelLow = $_POST['TeleFuelLow'];
$teleFuelHigh = $_POST['TeleFuelHigh'];
$teleFuelPoints = $_POST['TeleFuelPoints'];
$teleRotorEngaged = $_POST['TeleRotorEngaged'];
$endgame = $_POST['Hang'];
$playStyle = $_POST['PlayStyle'];

// This query inserts data into the database
$sql_query = "INSERT INTO scoutdb (TeamNumber, TeamName, AutoFuelLow, AutoFuelHigh, AutoFuelPoints, AutoRotorEngaged, TeleFuelLow, TeleFuelHigh, TeleFuelPoints, TeleRotorEngaged, Hang, PlayStyle) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE TeamName=VALUES(TeamName), AutoFuelLow=VALUES(AutoFuelLow), AutoFuelHigh=VALUES(AutoFuelHigh), AutoFuelPoints=VALUES(AutoFuelPoints), AutoRotorEngaged=VALUES(AutoRotorEngaged), TeleFuelLow=VALUES(TeleFuelLow), TeleFuelHigh=VALUES(TeleFuelHigh), TeleFuelPoints=VALUES(TeleFuelPoints), TeleRotorEngaged=VALUES(TeleRotorEngaged), Hang=VALUES(Hang), PlayStyle=VALUES(PlayStyle);";

// Prepare the SQL statement
if(!($stmt = $con->prepare($sql_query))) {
    echo "Statement could no be prepared: (".$con->errno.") ".$con->error;
} // End of if statement

// Bind parameters to the statement
if(!($stmt->bind_param("isssisssisss", $teamNumber, $teamName, $autoFuelLow, $autoFuelHigh, $autoFuelPoints,
    $autoRotorEngaged, $teleFuelLow, $teleFuelHigh, $teleFuelPoints, $teleRotorEngaged, $endgame, $playStyle))) {
    // Display a message if an error occurs when binding
    echo "Data could not be binded: (".$stmt->errno.") ".$stmt->error;;
} // End of if statement

// Execute statement
if (!($stmt->execute())) {
    // Display a message if an error occurs when executing
    echo "Execute failed: (".$stmt->errno.") ".$stmt->error;
} // End of if statement
?>

Table Structure

enter image description here

If you need any other files please let me know, thank you!
UPDATE: Still couldn't figure out the problem, I always end up switching back between data overwriting where it shouldn't or not updating at all

Samer Alabi
  • 117
  • 1
  • 10
  • 1
    Did you consider scenario when multiple Android clients generate same local ID with different data and upload it to server database? Last client will always override data produced by other clients. – hoaz Feb 28 '17 at 21:32
  • That scenario I haven't currently implemented but I have considered. The reason it's not implemented is because I want to know if the data is properly getting overwritten. It also isn't a priority for me because the group of people using the application will be constantly updating their data locally, so they need to overwrite old data in the server, even if two or more clients will be generating the same ID. – Samer Alabi Feb 28 '17 at 21:40
  • Your script is at risk of [SQL Injection Attack](http://stackoverflow.com/questions/60174/how-can-i-prevent-sql-injection-in-php) Have a look at what happened to [Little Bobby Tables](http://bobby-tables.com/) Even [if you are escaping inputs, its not safe!](http://stackoverflow.com/questions/5741187/sql-injection-that-gets-around-mysql-real-escape-string) Use [prepared parameterized statements](http://php.net/manual/en/mysqli.quickstart.prepared-statements.php) – RiggsFolly Mar 01 '17 at 01:20
  • After writing the prepared statements and attempting to change table structure again the application no longer overwrites the non duplicate keys but it doesn't update the columns anymore – Samer Alabi Mar 01 '17 at 12:40

0 Answers0