I have a Laravel app being served on Azure. I am using an AJAX request to poll data for a javascript chart.
The AJAX requests a URL defined in my routes (web.php), thus:
Route::get('/rfp_chart_data', 'DataController#chart_data')->name('chart_data');
That controller method runs a postgresql query and returns a JSON file. This all works fine.
However, after experiencing some performance issues, I decided to monitor the postgres queries and discovered that the same query was running 3 times for each request to this URL.
This happens regardless of whether I:
access the URL via an AJAX request
go directly to the URL in a browser
access the URL via cURL
This (AFAIK) eliminates the possibility that this is some sort of missing img src issue (e.g. What can cause a double page request?)
Thanks very much for any help...
EDIT:
Image of the duplicate queries in postgres pg_stat_activity -- this is from 1 web request:
EDIT:
Full controller code:
<?php
namespace App\Http\Controllers;
use App\AllRfpEntry;
use DB;
use Illuminate\Http\Request;
use Yajra\Datatables\Facades\Datatables;
class DataController extends Controller {
/**
* Displays datatables front end view
*
* #return \Illuminate\View\View
*/
//|| result_url || '\">' || result_title || '</a>'
public function chart_data(Request $request) {
$binding_array = array();
$chart_data_sql = "SELECT relevant_dates.date::date,
CASE WHEN award_totals.sum IS NULL
THEN 0
ELSE award_totals.sum
END
as sum
,
CASE WHEN award_totals.transaction_count IS NULL
THEN 0
ELSE award_totals.transaction_count
END
as transaction_count FROM
(
SELECT * FROM generate_series('" . date('Y-m-01', strtotime('-15 month')) . "'::date, '" . date('Y-m-01') . "'::date, '1 month') AS date
) relevant_dates
LEFT JOIN
(
SELECT extract(year from awarded_date)::text || '-' || RIGHT('0' || extract(month from awarded_date)::text, 2) || '-01' as date, sum(award_amount)::numeric as sum, COUNT(award_amount) as transaction_count FROM all_rfp_entries
WHERE awarded_date >= '" . date('Y-m-01', strtotime('-15 month')) . "'
AND awarded_date <= '" . date("Y-m-d") . "' AND award_status = 'AWARDED'
AND award_amount::numeric < 10000000000";
if ($request->get('rfp_company_filter')) {
$binding_array['rfp_company_filter'] = $request->get('rfp_company_filter');
$chart_data_sql .= " AND company = :rfp_company_filter";
};
if ($request->get('rfp_source_filter')) {
$binding_array['rfp_source_filter'] = $request->get('rfp_source_filter');
$chart_data_sql .= " AND rfp_source = :rfp_source_filter";
}
if ($request->get('exclude_fed_rev')) {
$chart_data_sql .= " AND rfp_source != 'US FED REV' ";
}
if ($request->get('rfp_year_filter')) {
$binding_array['rfp_year_filter'] = $request->get('rfp_year_filter');
$chart_data_sql .= " AND year = :rfp_year_filter";
}
if ($request->get('rfp_priority_level_filter')) {
$binding_array['rfp_priority_level_filter'] = $request->get('rfp_priority_level_filter');
$chart_data_sql .= " AND priority_level = :rfp_priority_level_filter";
}
if ($request->get('rfp_search_input_chart')) {
$binding_array['rfp_search_input_chart'] = $request->get('rfp_search_input_chart');
$chart_data_sql .= " AND search_document::tsvector ## plainto_tsquery('simple', :rfp_search_input_chart)";
}
$chart_data_sql .= " GROUP BY extract(year from awarded_date), extract(month from awarded_date)
) award_totals
on award_totals.date::date = relevant_dates.date::date
ORDER BY extract(year from relevant_dates.date::date), extract(month from relevant_dates.date::date)
";
return json_encode(DB::select($chart_data_sql, $binding_array));
}
public function data(Request $request) {
$query = AllRfpEntry::select('id', 'year', 'company', 'result_title', 'award_amount', 'edit_column', 'doc_type', 'rfp_source', 'posted_date', 'awarded_date', 'award_status', 'priority_level', 'word_score', 'summary', 'contract_age', 'search_document', 'link');
if ($request->get('exclude_na')) {
$query->where('all_rfp_entries.company', '!=', 'NA');
}
if ($request->get('clicked_date')) {
$query->where('all_rfp_entries.awarded_date', '>', $request->get('clicked_date'));
$query->where('all_rfp_entries.awarded_date', '<=', $request->get('clicked_date_plus_one_month'));
}
if ($request->get('filter_input')) {
$query->whereRaw("search_document::tsvector ## plainto_tsquery('simple', '" . $request->get('filter_input') . "')");
}
$datatables_json = datatables()->of($query)
->rawColumns(['result_title', 'edit_column', 'link'])
->orderColumn('award_amount', 'award_amount $1 NULLS LAST')
->orderColumn('priority_level', 'priority_level $1 NULLS LAST');
if (!$request->get('filter_input')) {
$datatables_json = $datatables_json->orderByNullsLast();
}
if (!$request->get('filter_input') and !$request->get('clicked_date')) {
$count_table = 'all_rfp_entries';
$count = DB::select(DB::raw("SELECT n_live_tup FROM pg_stat_all_tables WHERE relname = :count_table "), array('count_table' => $count_table))[0]->n_live_tup;
$datatables_json = $datatables_json->setTotalRecords($count);
}
$datatables_json = $datatables_json->make(true);
return $datatables_json;
}
}
EDIT:
An even crazier wrinkle...
I have a method in Laravel on this server pointed at a postgres database on another server to ingest new data. I just found out that even THAT method (the one pointing at the external server) is generating multiple queries on the external postsgres server!
Unless I'm missing something, this obviates an nginx issue or an issue with my provider (Azure), or a problem with any one specific method. Even a straight database connection over port 5432 (I'm assuming that's how Laravel accesses external databases) is generating the multiplier effect, so it must be something screwy with my Laravel installation... but no closer to figuring out what.
The best way to debug this is to start a debug session with xdebug and stepping through the code while you keep an eye on the stdout/logging output in a separate window.
Set a breakpoint on the first line in your controller, when it breaks there should be 0 queries done. If not you know something weird is going on in routing/the request. Then step through the function calls used in building the query.
It might be that one of the functions you use triggers executing the query (as suggested by Aaron Saray) or methods are missing (as suggested by Diogo Gomes) but this is hard to tell without knowing the code or stepping through an execution context step-by-step.
If you do not have a debugger you can always use dd($data); at any line to stop processing too and dump the data given. It will just take a little longer because you'll be doing a new request for each step in the code.
For what it's worth, I tracked this down to multiple postgres worker processes (which you can set in your postgres .conf file). So, not a bug, nor an issue with Laravel -- just parallel workers spawned by postgres.
Related
I must be missing something regarding how simultaneous requests are handled by PHP/Symfony, or perhaps how potentially simultaneous queries on the DB are handled...
This code seems to be doing the impossible - it randomly (about once a month) creates a duplicate of the new entity at the bottom. I conclude that it must happen when two clients make the same request twice, and both threads execute the SELECT query at the same time, picking up the entry where stop == NULL, and then they both (?) set the stoptime for that entry, and they both write a new entry.
Here's my logical outline as far as I understand things:
Get all entries with NULL for stoptime
Loop over those entries
Only proceed if the day of the entry (UTC) is different from current day (UTC)
Set stoptime for the open entry to 23:59:59 and flush to DB
Build a new entry with the starttime 00:00:00 on the next day
Assert that there are no other open entries in that position
Assert that there are no future entries in that position
Only then - flush the new entry to DB
Controller autocloseAndOpen
//if entry spans daybreak (midnight) close it and open a new entry at the beginning of next day
private function autocloseAndOpen($units) {
$now = new \DateTime("now", new \DateTimeZone("UTC"));
$repository = $this->em->getRepository('App\Entity\Poslog\Entry');
$query = $repository->createQueryBuilder('e')
->where('e.stop is NULL')
->getQuery();
$results = $query->getResult();
if (!isset($results[0])) {
return null; //there are no open entries at all
}
$em = $this->em;
$messages = "";
foreach ($results as $r) {
if ($r->getPosition()->getACRGroup() == $unit) { //only touch the user's own entries
$start = $r->getStart();
//Assert entry spanning datebreak
$startStr = $start->format("Y-m-d"); //Necessary for comparison, if $start->format("Y-m-d") is put in the comparison clause PHP will still compare the datetime object being formatted, not the output of the formatting.
$nowStr = $now->format("Y-m-d"); //Necessary for comparison, if $start->format("Y-m-d") is put in the comparison clause PHP will still compare the datetime object being formatted, not the output of the formatting.
if ($startStr < $nowStr) {
$stop = new \DateTimeImmutable($start->format("Y-m-d")."23:59:59", new \DateTimeZone("UTC"));
$r->setStop($stop);
$em->flush();
$txt = $unit->getName() . " had an entry in position (" . $r->getPosition()->getName() . ") spanning datebreak (UTC). Automatically closed at " . $stop->format("Y-m-d H:i:s") . "z.";
$messages .= "<p>" . $txt . "</p>";
//Open new entry
$newStartTime = $stop->modify('+1 second');
$entry = new Entry();
$entry->setStart( $newStartTime );
$entry->setOperator( $r->getOperator() );
$entry->setPosition( $r->getPosition() );
$entry->setStudent( $r->getStudent() );
$em->persist($entry);
//Assert that there are no future entries before autoopening a new entry
$futureE = $this->checkFutureEntries($r->getPosition(),true);
$openE = $this->checkOpenEntries($r->getPosition(), true);
if ($futureE !== 0 || $openE !== 0) {
$txt = "Tried to open a new entry for " . $r->getOperator()->getSignature() . " in the same position (" . $r->getPosition()->getName() . ") next day but there are conflicting entries.";
$messages .= "<p>" . $txt . "</p>";
} else {
$em->flush(); //store to DB
$txt = "A new entry was opened for " . $r->getOperator()->getSignature() . " in the same position (" . $r->getPosition()->getName() . ")";
$messages .= "<p>" . $txt . "</p>";
}
}
}
}
return $messages;
}
I'm even running an extra check here with the checkOpenEntries() to see whether there is at this point any entries with stoptime == NULL in that position. Initially, I thought that to be superflous because I thought that if one request is running and operating on the DB, the other request will not start until the first is finished.
private function checkOpenEntries($position,$checkRelatives = false) {
$positionsToCheck = array();
if ($checkRelatives == true) {
$positionsToCheck = $position->getRelatedPositions();
$positionsToCheck[] = $position;
} else {
$positionsToCheck = array($position);
}
//Get all open entries for position
$repository = $this->em->getRepository('App\Entity\Poslog\Entry');
$query = $repository->createQueryBuilder('e')
->where('e.stop is NULL and e.position IN (:positions)')
->setParameter('positions', $positionsToCheck)
->getQuery();
$results = $query->getResult();
if(!isset($results[0])) {
return 0; //tells caller that there are no open entries
} else {
if (count($results) === 1) {
return $results[0]; //if exactly one open entry, return that object to caller
} else {
$body = 'Found more than 1 open log entry for position ' . $position->getName() . ' in ' . $position->getACRGroup()->getName() . ' this should not be possible, there appears to be corrupt data in the database.';
$this->email($body);
$output['success'] = false;
$output['message'] = $body . ' An automatic email has been sent to ' . $this->globalParameters->get('poslog-email-to') . ' to notify of the problem, manual inspection is required.';
$output['logdata'] = null;
return $this->prepareResponse($output);
}
}
}
Do I need to start this function with some kind of "Lock database" method to achieve what I am trying to do?
I have tested all functions and when I simulate all kinds of states (entry with NULL for stoptime even when it shouldn't be able to be etc.) it all works out. And the majority of time it all works nicely, but that one day somewhere in the middle of the month, this thing happens...
You can never guarantee sequential order (or implicit exclusive access). Try that and you will dig yourself deeper and deeper.
As Matt and KIKO mentioned in the comments, you could use constraints and transactions and those should help immensely as your database will stay clean, but keep in mind your app needs to be able to catch errors produced by the database layer. Definitely worth trying first.
Another way of handling this is to enforce database/application level locking.
Database-level locking is more coarse and very unforgiving if you somewhere forget to release a lock (in long-running scripts).
MySQL docs:
If the connection for a client session terminates, whether normally or abnormally, the server implicitly releases all table locks held by the session (transactional and nontransactional). If the client reconnects, the locks are no longer in effect.
Locking the entire table is usually a bad idea altogether, but it is doable. This highly depends on the application.
Some ORMs, out-of-box, support object versioning and throw exceptions if the version has changed during the execution. In theory, your application would get an exception and upon retrying it would find that someone else already populated the field and that is no longer a candidate for an update.
Application-level locking is more fine-grained, but all points in the code need to honor the lock, otherwise, you are back to square #1. And if your app is distributed (say K8S, or just deployed across multiple servers), your locking mechanism has to be distributed as well (not local to an instance)
I'm currently struggling with an issue that is overloading my database which makes all page requests being delayed significantly.
Current scenario
- A certain Artisan Command is scheduled to be ran every 8 minutes
- This command has to update a whole table with more than 30000 rows
- Every row will have a new value, which means 30000 queries will have to be executed
- For about 14 seconds the server doesn't answer due to database overload (I guess)
Here's the handle method of the command handle()
public function handle()
{
$thingies = /* Insert big query here */
foreach ($thingies as $thing)
{
$resource = Resource::find($thing->id);
if(!$resource)
{
continue;
}
$resource->update(['column' => $thing->value]);
}
}
Is there any other approach to do this without making my page requests being delayed?
Your process is really inefficient and I'm not surprised it takes a long time to complete. To process 30,000 rows, you're making 60,000 queries (half to find out if the id exists, and the other half to update the row). You could be making just 1.
I have no experience with Laravel, so I'll leave it up to you to find out what functions in Laravel can be used to apply my recommendation. I just want to get you to understand the concepts.
MySQL allows you to submit a multi query; One command that executes many queries. It is drastically faster than executing individual queries in a loop. Here is an example that uses MySQLi directly (no 3rd party framework such as Laravel)
//the 30,000 new values and the record IDs they belong to. These values
// MUST be escaped or known to be safe
$values = [
['id'=>145, 'fieldName'=>'a'], ['id'=>2, 'fieldName'=>'b']...
];
// %s and %d will be replaced with column value and id to look for
$qry_template = "UPDATE myTable SET fieldName = '%s' WHERE id = %d";
$queries = [];//array of all queries to be run
foreach ($values as $row){ //build and add queries
$q = sprintf($qry_template,$row['fieldName'],$row['id']);
array_push($queries,$q);
}
//combine all into one query
$combined = implode("; ",$queries);
//execute all queries at once
$mysqli->multi_query($combined);
I would look into how Laravel does multi queries and start there. The last time I implemented something like this, it took about 7 milliseconds to insert 3,000 rows. So updating 30,000 will definitely not take 14 seconds.
As an added bonus, there is no need to first run a query to figure out whether the ID exists. If it doesn't, nothing will be updated.
Thanks to #cyclone comment I was able to update all the values in one single query.
It's not a perfect solution, but the query execution time now takes roughly 8 seconds and only 1 connection is required, which means the page requests are still being handled when the query is being executed.
I'm not marking this question as definitive since there might be improvements to make.
$ids = [];
$caseQuery = '';
foreach ($thingies as $thing)
{
if(strlen($caseQuery) == 0)
{
$caseQuery = '(CASE WHEN id = '. $thing->id . ' THEN \''. $thing->rank .'\' ';
}
else
{
$caseQuery .= ' WHEN id = '. $thing->id . ' THEN \''. $thing->rank .'\' ';
}
array_push($ids, $thing->id);
}
$caseQuery .= ' END)';
// Execute query
DB::update('UPDATE <table> SET <value> = '. $caseQuery . ' WHERE id IN ('. implode( ',' , $ids) .')');
We're running CakePHP v3.x with a Postgres database. I need to select some records whose latitude and longitude are within X distance from another point. PostGIS has a function that does just this but it seems that I have to right raw SQL queries in order to use it.
I'm not asking for help writing the raw query, but I am looking for confirmation as to whether the raw query approach is the correct way to use this extension while making best use of the framework. I searched and haven't found any libraries to extend the CakePHP ORM to include this. Perhaps there's a third option I haven't thought of.
[NOTE: This does not work...]
public function fetch()
{
$maxMetersAway = 10 * 1000;
$lat = $this->request->query['lat'];
$lng = $this->request->query['lng'];
$stops = $this->Stops->find('all')
->where("ST_DWithin( POINT($lng,$lat), POINT(Stops.lng,Stops.lat), $maxMetersAway")
->toArray();
$this->set(['stops'=>$stops, '_serialize' => true]);
}
I got the CakePHP query working with this:
$stops = $this->Stops->find('all')
->where(['ST_DWithin( ST_SetSRID(ST_MakePoint(' . $longitude . ', ' . $latitude . '),4326)::geography, ST_SetSRID(ST_MakePoint(Stops.longitude, Stops.latitude),4326)::geography, ' . $maxMetersAway . ')'])
->toArray();
Updated: Original didn't actually work with the $maxMetersAway properly.
We currently have a system, where we have a function that checks for date/time conflicts, that we manually populate all possible date/time conflicts as parameters to.
ie:
//session conflicts checker
function chkConflicts($sessions) {
if (!is_array($sessions)) {
$arrsessions = explode(',', $sessions);
} else {
$arrsessions = $sessions;
}
$conflictcount = 0;
foreach ($arrsessions as $thissession) {
if (($_POST[trim($thissession)] != '' && $_POST[trim($thissession)] != 0) || $_POST[trim($thissession) . '_faculty'] != '' && $_POST[trim($thissession) . '_faculty'] != 0) {
$conflictcount++;
}
}
if ($conflictcount > 1) {
return false;
} else {
return true;
}
}
used like so:
if (!chkConflicts('hours_5_p02_800, hours_5_p03_800, hours_5_p07_800, hours_5_p04_800')) {
$errmsg .= 'There is a conflict at 8:00am in the selections. ';
}
its very tedious, and time consuming to find these as well as manually populate the functions/params..
I need a new approach! I'm hoping to just get a list of the conflicts back and highlight the (table) row on the page with a message about you have conflicts in the highlighted areas (less specific then giving an exact time..etc, and still gets the job done for the user)
All the user interaction is done by checkboxes, that have a name that reflects the column in the table:
ie:
hours_5_p02_800, hours_5_p03_800, hours_5_p07_800,
hours_5_p04_800,hours_9_reg_session_300_845
(same names used in the chkConflicts function above)
I have (among others) two columns: sessiondate varchar(255) & presentationtime varchar(255) respectfully.
With the session date data looking like: 9/9/2015
And the presentation time data looking like: 8:45 AM - 9:05 AM (not sure if this matters, but including it for the sake of full disclosure)
I dont have ALOT of control over the database, but I could probably get the times split into two columns (start/end) if that would be best?
before ANY chkConflict function is called.. the 'selections' of the user are recorded/saved to the table.. AND THEN the conflict check is called.
//record hours for each day
function recordHours() {
$arrflds = explode(',', $_POST['fieldlist']);
$sql = "UPDATE {$this->eventcode}_cme SET";
foreach($arrflds as $key) {
$sql .= " " . addslashes(trim($key)) . " = '" . addslashes($_POST[trim($key)]) . "',";
}
$sql .= " lastupdated = '" . date('Y-m-d H:i:s') . "' WHERE id = '" . $_SESSION[$this->eventcode . '_id'] . "'";
$this->debugout .= ($this->debug) ? 'Record hours: ' . $sql . '<br>' . $this->crlf : '';
$result = mysql_query($sql) or exit('Error recording hours: ' . mysql_error());
}
*I'm updating things to PDO after I get the conflict stuff figured out. (thanks)
I dont mind this, because the chkConflict function (even though the choices have been saved) does NOT let the user move ahead until the error(s) message is taken care of (hence updating the table again when the conflicts are resolved)..
I'm thinking I'll need to no longer use the the chkConflict method and alter the recordHours function to not only update the table.. but because it has the 'fieldlist' array that was posted.. that I'll need to do the conflict checking there as well... or possibly call another function from withing recordHours and pass along the same fieldlist...
The column data is not really used for saving or (current) conflict checking of any sort... the column NAME is.
My problem is I'm not sure how to go do the date/time conflict check?
re-cap: fieldlist and column names are named like:
ie: hours_5_p02_800, hours_5_p03_800, hours_5_p07_800,hours_5_p04_800,hours_9_reg_session_300_845
(and is a 24 hour format for the time)
ex: hours_9_reg_session_300_845
9 = date
reg = event code
300 = session code
845 = session time(24-hour format)
Upon thinking more, its more like I need to do some sort of string parsing in PHP (on the fieldlist names) and do conversion/checking on that?
I need to take the string, break it down into its parts and do some sort of (concatenate/string building) comparison on it?
basically I get list of the fields being submitted that are formatted as above and match the table column names...
how can I pass this same fieldlist over to a new function (or whatever) to get any conflicts back?
Since you seem have the chance to change things a little I would suggest you to create the 2 fields but as timestamps field type. One for the start and one for the end.
You can look at timestamps fields as a date/time like 2015-10-17 08:45:00 or, using UNIX_TIMESTAMP('2015-10-17 08:45:00'), as an integer like 1445064300 which is exactly the same date/time info.
In both cases you can do things like
SELECT :yourdatetime BETWEEN date_time_start AND date_time_end;
or
SELECT :yourunixlikedatetime
NOT BETWEEN UNIX_TIMESTAMP(date_time_start) AND UNIX_TIMESTAMP(date_time_end);
for instance...
I have php application which gets information from a SAML POST and creates a record in the MySQL database, if the record is already present it just updates it
Here is the code
//getMemberRecord returns true for successful insertion.
$row = $this->getMemberRecord($data);
if ($row) {
//if the row already exists
$this->updateMemberRecord($data)
} else {
// creates a new record
$this->setMemberRecord($data);
}
This code is causing double inserts in the database, we don't have a unique key for the table due to some poor design constraints, but I see two HTTP posts in the access logs happening at the same time.
The create date column is same or differs by a second for the duplicate record.
This issue is happening for only select few, it works for most of them.
The table is innoDB table and we can not use sessions on our architecture.
Any ideas of why this would happen
You said:
I see two HTTP posts in the access logs
You should try avoiding this and have just one http POST invocation
May be it is a problem related to concurrency and mutual exclusion. The provided code must be executed in a mutually exclusion zone, so you must use some semaphore / mutex to prevent simultaneous execution.
If you have two HTTP POST happening your problem is not on the PHP/MYSQL side.
One thing is allowing a second 'transparent' HTTP POST in the HTTP protocol. It's the empty url. If you have an empty GET url in the page most browsers will replay the request which rendered the page. Some recent browser are not doing it, but most of them are still doing it (and it's the official way of HTTP). An empty GET url on a page is for example <img src=""> or < script url=""> but also an url() in a css file.
The fact you have one second between the two posts make me think it's what's happening for you. The POST response page is quite certainly containing an empty Get that the browser fill by replaying the POST... I hate this behaviour.
I found that the double inserts were happening becuase double submits and our application doesnot handle double submits efficiently, I read up on some articles on this, here are some of the solutions
it always best to handle double posts at the server side
best solution is to set a UNIQUE KEY on the table or do a INSERT ON DUPLICATE KEY UPDATE
if you have sessions then use the unique token , one of the technique in this article
http://www.freeopenbook.com/php-hacks/phphks-CHP-6-SECT-6.html
or use can use the Post/Redirect/Get technique which will handle most double submit problems
http://en.wikipedia.org/wiki/Post/Redirect/Get
note: the Double submit problem only happens on a POST request, GET request is immune
public function setMemberRecord($data, $brand_id, $organization_id, $context = null)
{
global $gRegDbManager;
$sql = "insert into member ......"
$gRegDbManager->DbQuery($sql);
// Popuplate the iid from the insert
$params['iid'] = $gRegDbManager->DbLastInsertId();
$data = some operations
return (int)$data;
}
public function getMemberRecord($field, $id, $brand_id, $organization_id, $organization_level_account = null)
{
global $gRegDbManager;
$field = mysql_escape_string($field);
$id = mysql_escape_string($id);
$sql = "SELECT * FROM " . DB_REGISTRATION_DATABASE . ".member WHERE $field = '$id' ";
if($organization_level_account) {
$sql .= "AND organization_fk = " . $organization_id;
} else {
$sql .= "AND brand_fk = " . $brand_id;
}
$sql .= " LIMIT 1";
$results = $gRegDbManager->DbGetAll($sql);
if(count($results) > 0) {
return $results[0];
}
return;
}
/* * ******************************************************************************************************
* Updates member record in the member table
* *******************************************************************************************************
*/
public function updateMemberRecord($id, $changes)
{
global $gRegDbManager;
$id = mysql_escape_string($id);
if(!empty($changes)) {
$sql = "UPDATE " . DB_REGISTRATION_DATABASE . ".member SET ";
foreach($changes as $field => $value) {
$sql .= mysql_escape_string($field) . " = '" . mysql_escape_string($value) . "', ";
}
$sql = rtrim($sql, ", ");
$sql .= " WHERE iid = '$id'";
$gRegDbManager->DbQuery($sql);
} else {
return false;
}
}