-
Notifications
You must be signed in to change notification settings - Fork 8.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Do data validation when undo. #1060
Changes from 1 commit
8ee4266
d004c1e
d2092fd
3a8631f
5ff3465
128369c
9d00eb5
a561bdf
9c1d120
766d3ca
108afe8
18ea10c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,14 +17,21 @@ | |
|
||
import java.sql.Connection; | ||
import java.sql.PreparedStatement; | ||
import java.sql.ResultSet; | ||
import java.sql.SQLException; | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
|
||
import com.alibaba.fastjson.JSON; | ||
import io.seata.common.util.StringUtils; | ||
import io.seata.rm.datasource.DataCompareUtils; | ||
import io.seata.rm.datasource.sql.struct.Field; | ||
import io.seata.rm.datasource.sql.struct.KeyType; | ||
import io.seata.rm.datasource.sql.struct.Row; | ||
import io.seata.rm.datasource.sql.struct.TableMeta; | ||
import io.seata.rm.datasource.sql.struct.TableRecords; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
/** | ||
* The type Abstract undo executor. | ||
|
@@ -34,6 +41,18 @@ | |
*/ | ||
public abstract class AbstractUndoExecutor { | ||
|
||
/** | ||
* Logger for AbstractUndoExecutor | ||
**/ | ||
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractUndoExecutor.class); | ||
|
||
/** | ||
* template of check sql | ||
* | ||
* TODO support multiple primary key | ||
*/ | ||
private static final String CHECK_SQL_TEMPLATE = "SELECT * FROM %s WHERE %S in (?)"; | ||
|
||
/** | ||
* The Sql undo log. | ||
*/ | ||
|
@@ -55,6 +74,15 @@ public AbstractUndoExecutor(SQLUndoLog sqlUndoLog) { | |
this.sqlUndoLog = sqlUndoLog; | ||
} | ||
|
||
/** | ||
* Gets sql undo log. | ||
* | ||
* @return the sql undo log | ||
*/ | ||
public SQLUndoLog getSqlUndoLog() { | ||
return sqlUndoLog; | ||
} | ||
|
||
/** | ||
* Execute on. | ||
* | ||
|
@@ -63,11 +91,11 @@ public AbstractUndoExecutor(SQLUndoLog sqlUndoLog) { | |
*/ | ||
public void executeOn(Connection conn) throws SQLException { | ||
|
||
// no need undo if the before data snapshot is equivalent to the after data snapshot. | ||
if (DataCompareUtils.isRecordsEquals(sqlUndoLog.getBeforeImage(), sqlUndoLog.getAfterImage())) { | ||
// when the data validation is not ok | ||
if (!dataValidation(conn)) { | ||
return; | ||
} | ||
dataValidation(conn); | ||
|
||
try { | ||
String undoSQL = buildUndoSQL(); | ||
|
||
|
@@ -136,9 +164,96 @@ protected void undoPrepare(PreparedStatement undoPST, ArrayList<Field> undoValue | |
* Data validation. | ||
* | ||
* @param conn the conn | ||
* @throws SQLException the sql exception | ||
* @return return true if data validation is ok and need continue | ||
* @throws SQLException the sql exception such as has dirty data | ||
*/ | ||
protected void dataValidation(Connection conn) throws SQLException { | ||
protected boolean dataValidation(Connection conn) throws SQLException { | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It ‘s better to add a switcher to close the dirty data check manually, the switcher is open default. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, I will add it in the next standalone PR. |
||
TableRecords beforeRecords = sqlUndoLog.getBeforeImage(); | ||
TableRecords afterRecords = sqlUndoLog.getAfterImage(); | ||
|
||
// Compare current data with before data | ||
// No need undo if the before data snapshot is equivalent to the after data snapshot. | ||
if (DataCompareUtils.isRecordsEquals(beforeRecords, afterRecords)) { | ||
return false; | ||
} | ||
|
||
// Validate if data is dirty. | ||
TableRecords currentRecords = queryCurrentRecords(conn); | ||
// compare with current data and after image. | ||
if (!DataCompareUtils.isRecordsEquals(afterRecords, currentRecords)) { | ||
|
||
// If current data is not equivalent to the after data, then compare the current data with the before | ||
// data, too. No need continue to undo if current data is equivalent to the before data snapshot | ||
if (DataCompareUtils.isRecordsEquals(beforeRecords, currentRecords)) { | ||
return false; | ||
} else { | ||
if (LOGGER.isDebugEnabled()) { | ||
LOGGER.debug("check dirty datas failed, old and new data are not equal," + | ||
"tableName:[" + sqlUndoLog.getTableName() + "]," + | ||
"oldRows:[" + JSON.toJSONString(afterRecords.getRows()) + "]," + | ||
"newRows:[" + JSON.toJSONString(currentRecords.getRows()) + "]."); | ||
} | ||
throw new SQLException("Has dirty records when undo."); | ||
} | ||
} | ||
return true; | ||
} | ||
|
||
/** | ||
* Query current records. | ||
* | ||
* @param conn the conn | ||
* @return the table records | ||
* @throws SQLException the sql exception | ||
*/ | ||
protected TableRecords queryCurrentRecords(Connection conn) throws SQLException { | ||
TableRecords undoRecords = getUndoRows(); | ||
TableMeta tableMeta = undoRecords.getTableMeta(); | ||
String pkName = tableMeta.getPkName(); | ||
String pkTypeName = tableMeta.getColumnMeta(pkName).getDataTypeName(); | ||
|
||
// build check sql | ||
String checkSQL = String.format(CHECK_SQL_TEMPLATE, sqlUndoLog.getTableName(), pkName); | ||
// parese pk values | ||
Object[] pkValues = parsePkValues(getUndoRows()); | ||
|
||
PreparedStatement statement = conn.prepareStatement(checkSQL); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The statement should close in finally . |
||
statement.setArray(1, conn.createArrayOf(pkTypeName, pkValues)); | ||
|
||
ResultSet checkSet = statement.executeQuery(); | ||
|
||
TableRecords currentRecords; | ||
try { | ||
currentRecords = TableRecords.buildRecords(tableMeta, checkSet); | ||
} finally { | ||
if (checkSet != null) { | ||
checkSet.close(); | ||
} | ||
} | ||
return currentRecords; | ||
} | ||
|
||
/** | ||
* Parse pk values object [ ]. | ||
* | ||
* @param records the records | ||
* @return the object [ ] | ||
*/ | ||
protected Object[] parsePkValues(TableRecords records) { | ||
String pkName = records.getTableMeta().getPkName(); | ||
List<Row> undoRows = records.getRows(); | ||
Object[] pkValues = new Object[undoRows.size()]; | ||
for (int i = 0; i < undoRows.size(); i++) { | ||
List<Field> fields = undoRows.get(i).getFields(); | ||
if (fields != null) { | ||
for (Field field : fields) { | ||
if (StringUtils.equalsIgnoreCase(pkName, field.getName())) { | ||
pkValues[i] = field.getValue(); | ||
} | ||
} | ||
} | ||
} | ||
return pkValues; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we throw some exception explicitly for the code change later?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I don’t get your point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I add some log and change the method name for more explicitly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as communicated offline
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@leizhiyuan done.