lundi 23 février 2015

Storing incomplete form data in operational database

Is it generally considered a good idea to keep data out of your operational database until it has passed your form validation rules?


I joined a new team that wants to save each part of a four part form to a related operational database table as it is entered by the user.


Personally I don't think you should store anything in your operational database until it passes all your form validation rules. Otherwise when you are processing your validated operational data you will have to have a way to filter out the incomplete form data - which I think is completely unnecessary.


OTHER THINGS TO CONSIDER:


This is for a very large organization (>1 million members) and using the session is not an option (server simply can't handle the load).


Form data is kept for up to 30 days. The form may change after a user has started. So when the user resumes these form changes should not cause problems.


CODE EXAMPLE


My proposal was to keep all the form data in memory as a JSON object. If the user saves before submitting we store the entire form as a JSON object in a large VARCHAR or CLOB. The different parts of the form are stored as an array of strings (i.e. FORM_DATA). The JSON object keys come from the form element id attribute. For parts that can repeat there is a group identifier (i.e. FORM_DATA_GROUP). Validation rules are defined by an array that maps the form element id to an array of JavaScript functions (i.e. FORM_RULES).

My example HTML and JavaScript follows:



<!DOCTYPE html>
<html>
<head>
<title>eApplication Form on ePortal POC</title>
<style>
*
{
margin: 0;
padding: 0;
}

ul
{
margin: 0.25em;
list-style-type: none;
}

li
{
margin-left: 0.5em;
}

textarea
{
-webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
-moz-box-sizing: border-box; /* Firefox, other Gecko */
box-sizing: border-box; /* Opera/IE 8+ */
}

input, p
{
padding: 0.15em;
}
</style>
</head>
<body style="height: 100%; width: 100%;">
<header style="position: absolute; left: 0; right: 0; height: 3em; background-color: blue;">
<h1 style="text-align: center;">ePortal Header</h1>
</header>

<nav style="position: absolute; top: 3em; bottom: 3em; width: 12em; background-color: gray;">
<ul>
<li><a href="javascript:loadFormData();">Load Form Data</a></li>
</ul>
</nav>

<section style="position: absolute; top: 3em; bottom: 3em; right: 0; left: 12em;">
<script>
var GROUP_CONTROLS = "<input type='button' onclick='addGroup();' value='Add'><input type='button' onclick='deleteGroup();' value='Delete'><input type='button' onclick='goToGroup(1);' value='Next'><input type='button' onclick='goToGroup(-1);' value='Previous'>";

var FORM_PAGES = [
"<p>First Name:<input type='text' name='first_name' id='first_name' /></p><p>Last Name:<input type='text' name='last_name' id='last_name' /></p><p>Telephone:<input type='text' name='telephone' id='telephone' /></p><p>Email:<input type='text' name='email' id='email' /></p>",
"<p>Line 1:<input type='text' name='address_line_1' id='address_line_1' /></p><p>Line 2:<input type='text' name='address_line_2' id='address_line_2' /></p><p>From:<input type='text' name='address_from' id='address_from' /></p><p>To:<input type='text' name='address_to' id='address_to' /></p>",
"<p>Employer:<input type='text' name='employer' id='employer' /></p><p>From:<input type='text' name='employer_from' id='employer_from' /></p><p>To:<input type='text' name='employer_to' id='employer_to' /></p>" ];
var FORM_PAGES_INDEX = 0;
var FORM_DATA_GROUP = [ "", "address_history", "employer_history" ];
var FORM_DATA_GROUP_INDEX = 0;
var FORM_DATA = {};
var FORM_RULES = {"first_name" : [ mandatory ], "email" : [ mandatory, email ]};

function loadFormData()
{
FORM_DATA = JSON.parse(document.getElementById("JSON_viewer").value);

displayFormPage(0);
}

function displayFormPage(inc)
{
FORM_PAGES_INDEX += inc;
FORM_PAGES_INDEX = Math.max(0, Math.min(FORM_PAGES_INDEX, FORM_PAGES.length - 1));
FORM_DATA_GROUP_INDEX = 0;

setFormPageContent();
};

function setFormPageContent()
{
var page = FORM_PAGES[FORM_PAGES_INDEX];

page += FORM_DATA_GROUP[FORM_PAGES_INDEX].length > 0 ? GROUP_CONTROLS : "";

document.getElementById('form_content').innerHTML = page;

var ds = getDataFormSection();

for(var i = 0; i < ds['keys'].length; i++)
{
var field = document.getElementById(ds['keys'][i]);

if(field)
field.value = ds['data'][ds['keys'][i]];
else
console.log("Field not found: [" + ds['keys'][i] + " : " + ds['data'][ds['keys'][i]] + "]");
}
};

function getDataFormSection()
{
var keys = [];
var data = {};

if(FORM_DATA_GROUP[FORM_PAGES_INDEX].length > 0)
data = FORM_DATA[FORM_DATA_GROUP[FORM_PAGES_INDEX]][FORM_DATA_GROUP_INDEX];
else
data = FORM_DATA;

data = data ? data : {};
keys = Object.keys(data);

return {"keys" : keys, "data" : data};
}

var focusHandler = function handleBlur(event)
{
if (event.target.id.length == 0)
return;

if (FORM_DATA_GROUP[FORM_PAGES_INDEX].length > 0)
{
if (!FORM_DATA[FORM_DATA_GROUP[FORM_PAGES_INDEX]])
FORM_DATA[FORM_DATA_GROUP[FORM_PAGES_INDEX]] = [];

if (FORM_DATA[FORM_DATA_GROUP[FORM_PAGES_INDEX]][FORM_DATA_GROUP_INDEX] == null)
FORM_DATA[FORM_DATA_GROUP[FORM_PAGES_INDEX]][FORM_DATA_GROUP_INDEX] = {};

FORM_DATA[FORM_DATA_GROUP[FORM_PAGES_INDEX]][FORM_DATA_GROUP_INDEX][event.target.id] = event.target.value;
}
else
FORM_DATA[event.target.id] = event.target.value;

var rules = FORM_RULES[event.target.id];

if (rules)
{
for (var i = 0; i < rules.length; i++)
rules[i](event.target);
}

document.getElementById('JSON_viewer').value = JSON.stringify(FORM_DATA);
};

function addGroup()
{
if(FORM_DATA[FORM_DATA_GROUP[FORM_PAGES_INDEX]][FORM_DATA_GROUP_INDEX] == null)
return;

FORM_DATA_GROUP_INDEX = FORM_DATA[FORM_DATA_GROUP[FORM_PAGES_INDEX]].length;

setFormPageContent();
};

function deleteGroup()
{
FORM_DATA[FORM_DATA_GROUP[FORM_PAGES_INDEX]].splice(FORM_DATA_GROUP_INDEX, 1);

FORM_DATA_GROUP_INDEX = 0;

setFormPageContent();

document.getElementById('JSON_viewer').value = JSON.stringify(FORM_DATA);
}

function goToGroup(dir)
{
var group_size = FORM_DATA[FORM_DATA_GROUP[FORM_PAGES_INDEX]].length;

FORM_DATA_GROUP_INDEX += dir;
FORM_DATA_GROUP_INDEX = Math.max(0, Math.min(FORM_DATA_GROUP_INDEX, group_size - 1));

setFormPageContent();
}

function mandatory(src)
{
if (src.value.length == 0)
alert(src.id + " is mandatory");
};

function email(src)
{
var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;

if (!re.test(src.value))
alert(src.id + " is not a valid email address");
};
</script>

<h1 style="text-align: center;">Welcome to eApplication!</h1>

<div id="form_content" style="padding: 1em;"></div>

<input type="button" onclick="displayFormPage(-1);" value="...Previous Section">
<input type="button" onclick="displayFormPage(1);" value="Next Section...">

<p style="position: absolute; bottom: 5em; left: 0; right: 0; text-align: center;">JSON Viewer & Editor</p>

<div style="position: absolute; bottom: 0; left: 0; right: 0; height: 5em; background-color: green;">
<textarea id="JSON_viewer" style="width: 100%; height: 100%;"></textarea>
</div>

<script>
document.getElementById("form_content").addEventListener("blur", focusHandler, true);
displayFormPage(0);
</script>
</section>

<footer style="position: absolute; bottom: 0; left: 0; right: 0; height: 3em; background-color: blue;">
</footer>
</body>
</html>


You can test what happens when a form changes by copying the following to the JSON view and clicking load form data.


{"first_name":"Crazy","last_name":"Bugger","eye_color":"blue","telephone":"012-345-6789","email":"me@home.ca","address_history":[{"address_line_1":"Emard","address_line_2":"","address_from":"now","address_to":"then"},{"address_line_1":"Gordon","address_line_2":"QC","address_from":"too","address_to":"long"}]}





Aucun commentaire:

Enregistrer un commentaire