I have a small Android app and currently I am firing a sql statement in android to get the count of rows in database for a specific where clause.
Following is my sample code:
public boolean exists(Balloon balloon) {
if(balloon != null) {
Cursor c = null;
String count_query = "Select count(*) from balloons where _id = ?";
try {
c = getReadableDatabase().rawQuery(count_query, new String[] {balloon.getId()});
if (c.getCount() > 0)
return true;
} catch(SQLException e) {
Log.e("Running Count Query", e.getMessage());
} finally {
if(c!=null) {
try {
c.close();
} catch (SQLException e) {}
}
}
}
return false;
}
This method returns me a count 1 even when the database table is actually empty. I am not able to figure out; why that would happen. Running the same query in database gets me a count of 0.
I was wondering if it's possible to log or see in a log, all the sql queries that eventually get fired on database after parameter substitution; so that I can understand what's going wrong.
Cheers
That query will always return one record (as will any SELECT COUNT). The reason is, that the record it returns contains a field that indicates how many records are present in the "balloons" table with that ID.
In other words, the one record (c.getcount()==1) is NOT saying that the number of records found in balloons is one, rather it is the record generated by Sqlite which contains a field with the result.
To find out the number of balloons, you should c.movetofirst() and then numberOfBalloons = c.getInt(0)
Related
Problem: I can't seem to get an imported database to fail in Android Room until first queried? I'm wanting to try/catch and validate a database and I'm not succeeding on catching it before the first query. I always thought Android Room validated the database at the moment of instance creation and build against the schema, but apparently not. So the database fails upon first query.
What I'm trying to do: This app manages multiple databases that can be shared between users. So databases can be imported or exported. I suspect that at some point, someone will attempt to import the wrong database and or structure or do something to cause it to fail. I'm trying to catch the failure at the instance / build of the database or sooner.
What I've tried: I have a try/catch/finally block at the first instance creation, but it is only failing when first queried... then it notices that a table or column is missing. I'd like to catch it sooner if possible. I've looked at the RoomDatabase methods but nothing specifically applies to validation that I see other than just letting it break.
I always thought Android Room validated the database at the moment of instance creation and build against the schema, but apparently not.
The database validation is part of the open process, which does not happen until you actually try to access the database, as opposed to when getting the instance.
I can't seem to get an imported database to fail in Android Room until first queried?
When you get the instance you can force an open by getting (or trying to get) a SupportSQLiteDatabase by using either getWritableDatabase or getReadableDatabase via the instance's openHelper.
e.g.
(Kotlin)
db = TheDatabase.getInstance(this)
try {
val supportDB = db.openHelper.writableDatabase
}
catch(e: Exception) {
....
}
(Java)
db = TheDatabase.getInstance(this);
try {
SupportSQLiteDatabase supportDB = db.getOpenHelper().getWritableDatabase();
}
catch (Exception e) {
....
}
Alternative - Self validation
You could also do your own validation and thus avoid an exception (if the validation is simple enough). You could also make corrections thus allowing perhaps minor transgressions to be acceptable.
Before getting the actual instance, you could get a validation instance (different database name) that is created as per Room and then compare the schemas yourself.
here's an example designed to detect a table missing by creating the real database with a different table name (nottablex instead of tableX).
The TableX entity :-
#Entity
class TableX {
#PrimaryKey
Long id=null;
String name;
String other;
}
No Dao's as not needed for the example.
The TheDatabase with get instance methods, one for normal, the other for getting another (validation (empty model for schema comparison)) database but as an SQLiteDatabase.
#Database(entities = {TableX.class},version = 1)
abstract class TheDatabase extends RoomDatabase {
private static volatile TheDatabase instance = null;
private static volatile TheDatabase validationInstance = null;
static TheDatabase getInstance(Context context, String databaseName) {
if (instance == null ) {
instance = Room.databaseBuilder(context,TheDatabase.class,databaseName)
.allowMainThreadQueries()
.build();
}
return instance;
}
static SQLiteDatabase getValidationInstance(Context context, String databaseName) {
// Delete DB if it exists
if (context.getDatabasePath(databaseName).exists()) {
context.getDatabasePath(databaseName).delete();
}
// Create database and close it
TheDatabase db = Room.databaseBuilder(context,TheDatabase.class,databaseName)
.allowMainThreadQueries()
.build();
db.getOpenHelper().getWritableDatabase();
db.close();
return SQLiteDatabase.openDatabase(context.getDatabasePath(databaseName).getPath(),null,SQLiteDatabase.OPEN_READWRITE);
}
}
note that this forces the open to create the model/validation database (else the openDatabase would fail).
And finally for the demo MainActivity which creates an invalid database and then goes on to do a simple validation (just check that the expected tables exist). Only opening (never in the case of the example) the database if the tables expected by room exist.
public class MainActivity extends AppCompatActivity {
public static final String DATABASE_NAME = "the_database.db";
public static final String VALIDATION_DATABASE_NAME = DATABASE_NAME + "_validation";
public static final String TAG = "DBVALIDATION";
TheDatabase db;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
createIncorrectDatabase(this,DATABASE_NAME);
if (validateDatabase(VALIDATION_DATABASE_NAME,DATABASE_NAME) < 0) {
Log.d(TAG,"DATABASE " + DATABASE_NAME + " does mot match model.");
} else {
/* Database validated OK so use it */
db = TheDatabase.getInstance(this,DATABASE_NAME);
}
}
/* Purposefully create invalid database */
private void createIncorrectDatabase(Context context, String databaseName) {
File dbfile = context.getDatabasePath(databaseName);
if (!dbfile.exists()) {
dbfile.getParentFile().mkdirs();
}
SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(context.getDatabasePath(databaseName),null);
db.execSQL("CREATE TABLE IF NOT EXISTS nottablex(id INTEGER PRIMARY KEY,name TEXT)");
db.close();
}
#SuppressLint("Range")
private long validateDatabase(String modelDatabase, String actualDatabase) {
String sqlite_master = "sqlite_master";
/* Want to skip room_master_table and sqlite tables susch as sqlite_sequence */
/* in this example only checking tables to show the basic technique */
String wherecluase = "name NOT LIKE 'sqlite_%' AND name NOT LIKE 'room_%' AND type = 'table'";
long rv = 0;
/* Get the model/validation database */
SQLiteDatabase modelDB = TheDatabase.getValidationInstance(this,modelDatabase);
/* Only need to check if the database exists as otherwise it will be created according to Room */
if (this.getDatabasePath(actualDatabase).exists()) {
/* Open as an SQLiteDatabase so no Room open to throw an exception */
SQLiteDatabase actualDB = SQLiteDatabase.openDatabase(this.getDatabasePath(actualDatabase).getAbsolutePath(),null,SQLiteDatabase.OPEN_READWRITE);
/* Get the tables expected from the model Room database */
Cursor modelTableNames = modelDB.query(sqlite_master,null,wherecluase,null,null,null,null);
Cursor actualTableNames = null; /* prepare Cursor */
/* Loop through the tables names in the model checking if they exist */
while (modelTableNames.moveToNext()) {
/* See if the expected table exists */
actualTableNames = actualDB.query(sqlite_master,null,"name=?",new String[]{modelTableNames.getString(modelTableNames.getColumnIndex("name"))},null,null,null);
if (!actualTableNames.moveToFirst()) {
Log.d(TAG,"Table " + modelTableNames.getString(modelTableNames.getColumnIndex("name")) + " not found.");
rv = rv -1; /* Table not found so decrement rv to indicate number not found */
}
}
/* Close the actualTableNames Cursor if it was used */
if (actualTableNames != null) {
actualTableNames.close();
}
/* Close the modelTableNames Cursor */
modelTableNames.close();
/* Close the actual database so Room can use it (comment out to show results in database Inspector)*/
actualDB.close();
} else {
Log.d(TAG,"Actual Database " + actualDatabase + " does not exist. No validation required as it would be created");
}
/* Close and delete the model database (comment out to show results in database Inspector)*/
modelDB.close();
this.getDatabasePath(modelDatabase).delete();
return rv;
}
}
Result
The log includes :-
D/DBVALIDATION: Table TableX not found.
D/DBVALIDATION: DATABASE the_database.db does mot match model.
Bypassing the close and model delete the databases for the above are:-
Note in this simple example Room would actually create the TableX table rather than fail with an exception.
I have an Apex class whose purpose it is to retrieve and delete overdue tasks on the contact role (related to an account) that the user just called. I need to modify it so that it queries for all overdue tasks assigned to the user on ALL contact roles on the account but am struggling to get the right query.
Here is a portion of the code in question, the part I think is most relevant:
/***************************************************
Brief Description: Deletes overdue Tasks or Events.
****************************************************/
public class DSDenali_DeleteOverDueActivities {
private static List<Task> followUpTasksToDelete = new List<Task>();
private static List<Event> followUpEventsToDelete = new List<Event>();
private static Map<Id, Set<String>> ownerOfActivities = new Map<Id, Set<String>>();
#InvocableMethod (label = 'DialSource Denali Delete Overdue Activities')
public static void gatherData(List<Data> requests)
{
Map<Id, String> results = new Map<Id, String>();
for (Data request : requests)
results.put(request.contactRoleID, request.assignedTo);
for (Id key : results.keySet())
{
Set<String> assignedToValues = parseAssignedTo(results.get(key));
System.debug('assignedToValues: ' + assignedToValues);
ownerOfActivities.put(key, assignedToValues);
System.debug(ownerOfActivities);
}
queryAndFilterData();
deleteOverdueActivities();
}
//Query for the Tasks and Events and filter the ones to delete
private static void queryAndFilterData()
{
List<Contact_Role__c> contactRoles = [SELECT Id,
(SELECT Id, Owner.UserRole.Name, OwnerId FROM Tasks WHERE status != 'Completed' AND ActivityDate <= :system.TODAY() AND Type__c = 'Outbound Call'),
(SELECT Id, Owner.UserRole.Name, OwnerId, Description FROM Events WHERE EndDateTime <= :system.NOW())
FROM Contact_Role__c
WHERE Id IN :ownerOfActivities.keySet()];
for (Contact_Role__c contactRole : contactRoles)
{
for (Task currentTask : contactRole.Tasks)
{
if (ownerOfActivities.get(contactRole.Id).contains(currentTask.OwnerId))
{
if (currentTask.OwnerId != '0050B000006ET37' && currentTask.Owner.UserRole != NULL && Pattern.matches('.*Altair.*', currentTask.Owner.UserRole.Name))
followUpTasksToDelete.add(currentTask);
else if (currentTask.OwnerId == '0050B000006ET37')
followUpTasksToDelete.add(currentTask);
else
continue;
}
else if (ownerOfActivities.get(contactRole.Id).contains('ALL'))
{
if (currentTask.Owner.UserRole != NULL && Pattern.matches('.*Altair.*', currentTask.Owner.UserRole.Name))
followUpTasksToDelete.add(currentTask);
else
continue;
}
}
for (Event currentEvent : contactRole.Events)
{
if (ownerOfActivities.get(contactRole.Id).contains(currentEvent.OwnerId) && currentEvent.Description == NULL)
{
if (currentEvent.OwnerId != '0050B000006ET37' && currentEvent.Owner.UserRole != NULL && Pattern.matches('.*Altair.*', currentEvent.Owner.UserRole.Name))
followUpEventsToDelete.add(currentEvent);
else if (currentEvent.OwnerId == '0050B000006ET37')
followUpEventsToDelete.add(currentEvent);
else
continue;
}
else if (ownerOfActivities.get(contactRole.Id).contains('ALL') && currentEvent.Description == NULL)
{
if (currentEvent.Owner.UserRole != NULL && Pattern.matches('.*Altair.*', currentEvent.Owner.UserRole.Name))
followUpEventsToDelete.add(currentEvent);
else
continue;
}
}
}
}
//Delete overdue Events/Tasks
private static void deleteOverdueActivities()
{
try{
delete followUpTasksToDelete;
}
catch (DmlException e){
System.debug('The following error occured (DSDenali_DeleteOverDueActivities): ' + e);
}
try{
delete followUpEventsToDelete;
}
catch (DmlException e){
System.debug('The following error occured (DSDenali_DeleteOverDueActivities): ' + e);
}
}
//Parse the CSVs of possible owners
private static Set<String> parseAssignedTo(String assignedTo)
{
Set<String> assignedToValues = new Set<String>();
assignedToValues.addAll(assignedTo.deleteWhitespace().split(','));
return assignedToValues;
}
public class Data
{
#InvocableVariable (required=true)
public String assignedTo;
#InvocableVariable (required=false)
public Id contactRoleID;
}
}
(updated after OP posted more code and asked for code review)
It's not bad code, could use some comments. Consider posting it in https://codereview.stackexchange.com/ (although not many SF-related posts end up there) or on https://salesforce.stackexchange.com
gatherData()
Your input variable (after some parsing) is Map<Id, Set<String>> where key is contact role's id. That set of strings for users (owners) is bit misleading. At a glance you immediately ask yourself why it can't be Set<Id>. Only deep down in the code you see that apparently "All" is one of allowed values. This is... not great. I'd be tempted to make 2 methods here, one taking legit Map<Id, Set<Id>> and other taking simply Set<Id> if you know that you're effectively skipping the second parameter.
queryAndFilterData()
You have only one query and it's not in a loop, very good. My trick (from before edit) won't work for you (you don't really have the Account__c or however is the field called anywere in the input, you have only record ids. If you want to check / delete all tasks for roles under this account cleanest might be to use two queries
// 1st the helper to collect all accounts...
Set<Id> allAccounts = new Map<Id, Account>([SELECT Id
FROM Account
WHERE Id IN (SELECT Account__c FROM Contact_Role__c WHERE Id IN :ownerOfActivities.keyset()]).keyset();
// then the outline of the main query would be bit like this
SELECT Id,
(SELECT ... FROM Tasks WHERE ...),
(SELECT ... FROM Events WHERE ...)
FROM Contact_Role__c
WHERE Account__c IN :allAccounts
AND ...
I'd check how much of this filtering logic could be pushed to the query itself rather than manually inspecting each returned row. I mean look at that:
Imagine we're going the simple route (ignoring concept of "All" users) and let's say you have another Set<Id> allUsers; variable (composed out of all ids mentioned in all your data pieces)
The query for Tasks could become something as simple as
(SELECT Id
FROM Tasks
WHERE Status != 'Completed'
AND ActivityDate <= TODAY
AND Type__c = 'Outbound Call'
AND OwnerId IN :allUsers
AND (OwnerId = '0050B000006ET37' OR Owner.UserRole.Name LIKE '%.Altair.%')
)
You still have to loop through them to verify whether each can really really be deleted (it's not enough to match out of all users, it has to check also if it's OK for user on this particular Contact_Role__c, right?) but something like that should return less rows and no more regular expression matching... should be bit faster.
I wouldn't have a magic variable for that one special owner's id like that. Ideally there would be something else that describes this special user (role? profile? custom field on User record? permission to "Author Apex" in profile?). At the very least move it to helper Id variable at top of the file so it's not copy-pasted all over the place. And ask your business user what should happen if that guy (default task owner? some integration account?) ever leaves the company because poo poo will hit the propeller hard.
If you'll get comfortable with this version of the query the "ALL" version would become even simpler? No "all users", no "magic id" & job done?
(SELECT Id
FROM Tasks
WHERE Status != 'Completed'
AND ActivityDate <= TODAY
AND Type__c = 'Outbound Call'
Owner.UserRole.Name LIKE '%.Altair.%'
)
Don't trust random guy on internet who doesn't know your business process but yeah, there's some room for improvement. Just test thoroughly :)
deleteOverdueActivities()
This is not great try-catch pattern. You just raise it in debug log but silently swallow the error. Make it fail hard (let the error bubble up to user) or do some proper error handling like inserting something to helper Log__c object maybe or sending an email / chatter post to admin...
parseAssignedTo()
Not bad. I expect it explodes horribly when you pass a null to it. You're kind of protected from it by marking the variable required in last lines but I think this annotation applies only to Flows. Calling it from other Apex code isn't protected enough.
I'm trying to insert a new record using Entity Framework using the following code. Within the BuildRequest.Save() method in the BuildRequest class, when the 'Insert' occurs, and the db.Save() is done, a JobId is correctly generated for the BuildRequest (indicating that the Insert is being done correctly), however the record isn't added to the database.
When I check in SQL profiler, here's what the insert is trying to do:
exec sp_executesql N'INSERT [dbo].[BuildQueue]([ApplicationId], [BuildReason])
VALUES (#0, #1)
SELECT [JobId]
FROM [dbo].[BuildQueue]
WHERE ##ROWCOUNT > 0 AND [JobId] = scope_identity()',N'#0 int,#1 tinyint,#0=5819,#1=0
Here is my call to create a new 'job':
BuildRequest job = new BuildRequest(ApplicationId, Core.BuildReasons.NewApp);
job.Save()
which uses the following class:
public class BuildRequest
{
private Data.BuildQueue _buildRequest;
public BuildRequest(int applicationId, BuildReasons reason)
{
_buildRequest = new Data.BuildQueue();
ApplicationId = applicationId;
BuildReason = reason;
}
public int JobId
{
get { return _buildRequest.JobId; }
}
public int ApplicationId
{
get
{
return _buildRequest.ApplicationId;
}
set
{
_buildRequest.ApplicationId = value;
}
}
public BuildReasons BuildReason
{
get
{
return (BuildReasons)_buildRequest.BuildReason;
}
set
{
_buildRequest.BuildReason = (byte)value;
}
}
public int Save()
{
using (Core.Data.UnitOfWork db = new Data.UnitOfWork())
{
Data.BuildQueue buildRequest = db.BuildQueueRepository.Get(a => a.JobId == this.JobId).SingleOrDefault();
if (buildRequest == null)//current build request doesn't already exist
{
db.BuildQueueRepository.Insert(_buildRequest);
}
else // we're editing an existing build request
{
db.BuildQueueRepository.Update(_buildRequest);
}
db.Save();
// **At this point, _buildRequest has a JobId, but no record is added to the database
return JobId;
}
}
}
What I've checked so far:
the 'UnitOfWork' object I create ('db'), is connected to the correct database (a remote SQL server database)
the db.BuildQueueRepository.Insert(...) is correctly calling dbSet.Add(entity);
the db.Save() is correctly calling context.SaveChanges();
there's existing code elsewhere in my application that uses the same generic UnitOfWork repository, which appears almost identical to the code above, and it works perfectly
What could be happening? What could be the difference in the code that's working correctly?
Doh! There's a job that takes new records out of the queue if they're in a certain status. It turns out that this particular routine sets that status immediately and the record was being entered then immediately moved to a different table.
This is my first attempt in trying to query our LDAP server for AD info. When I am trying to query the LDAP server here is what I'm trying to retrieve:
I am trying to retrieve all active employees with a countlimit of 500 records whose displayname starts with "sav", has an email address and has a userAccountControl attribute of 512. The problem I'm encountering is that I'm only getting back 8 records total. I should literally be getting back at least 10 records.
I did a separate search on the 2 records that were NOT retrieved in my search and each had an email address and a userAccountControl value of 512. So I'm not sure why those 2 records were missing.
I'm sure I've done something wrong in my syntax but I cannot find what it is. Any HELP/DIRECTION would be appreciated. Thank you.
After googling I've defined the SEARCH FILTER as:
String searchFilter = "(&(objectClass=user)(displayname="+displayname+"*"+")(mail=*)(userAccountControl=512))";
Please see my complete method below:
public List<String> getAutocompleteEmpRecordsList(String displayname, LdapContext ctx) {
List<String> activeEmpAttributes = new ArrayList<String>();
Attributes attrs = null;
int count = 0;
int empEmailAddrLen = 0;
try {
SearchControls constraints = new SearchControls();
constraints.setCountLimit(500);
constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
String[] attrIDs = {"displayname", "mail", "userAccountControl"};
constraints.setReturningAttributes(attrIDs);
String searchFilter = "(&(objectClass=user)(displayname="+displayname+"*"+")(mail=*)(userAccountControl=512))";
NamingEnumeration answer = ctx.search("OU=Standard,OU=Users,DC=xxx,DC=org", searchFilter, constraints);
if (answer != null) {
while (answer.hasMore()) {
attrs = ((SearchResult) answer.next()).getAttributes();
if (attrs.get("displayname") != null) {
int empNameLen = attrs.get("displayname").toString().length();
activeEmpAttributes.add(attrs.get("displayname").toString().substring(13, empNameLen));
}
count++;
ctx.close();
}
}
else {
throw new Exception("Invalid User");
}
System.out.println("activeEmpAttributes: " + activeEmpAttributes);
System.out.println("count: " + activeEmpAttributes.size());
} catch (Exception ex) {
ex.printStackTrace();
}
return activeEmpAttributes;
}
You may be confusing displayname attribute and cn attribute.
On Windows server you've got a command line tool called LDIDIFDE.EXE which can allow you to test your filter.
ldifde -f datas.ldf -d "OU=Standard,OU=THR Users,DC=txhealth,DC=org" -r "(&(objectClass=user)(displayname=sav*)(mail=*)(userAccountControl=512))"
ldifde -f datas.ldf -d "OU=Standard,OU=THR Users,DC=txhealth,DC=org" -r "(&(objectClass=user)(cn=sav*)(mail=*)(userAccountControl=512))"
In the User and computer MMC you can also test your filter.
Start User and computer Active-Directory :
Right buton on registered request :
Choose personalize search, you've got an helper tab for common attributes :
You can choose personalized tab for technical attributes
You can test en copy the resulting LDAP filter (you don't need the double (& one is enough):
Can you post your userAccountControl, displayName, and mail values for the two excluded users?
FWIW the medial search on displayName would run alot faster if you add a tuple index to it.
I downloaded a free AD tool to view all in AD that I needed and it showed me that the data was not the problem but I was just not hitting all the OU's that I needed because there is NOT just 1 OU where all our users are stored.
Consequently, after googling some more I found a page on the Oracle site regarding LDAP and I changed my LDAPContext to DirContext for my connection to do searches within the directory as well as using this context's REFERRAL and set the value to "follow" to avoid the PartialSearchException.
I thought I'd post my findings just in case some other newbie ran into the same issue.
If you see a downside to the changes I made please let me know. Regards.
Here is my corrected code:
public List<String> getAutocompleteEmpRecordsList(String displayname, DirContext ctx) {
List<String> activeEmpAttributes = new ArrayList<String>();
Attributes attrs = null;
int count = 0;
int empEmailAddrLen = 0;
try {
SearchControls constraints = new SearchControls();
constraints.setCountLimit(500);
constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
String[] attrIDs = {"displayname", "mail", "userAccountControl"};
constraints.setReturningAttributes(attrIDs);
String searchFilter = "(&(objectClass=user)(displayname="+displayname.trim()+"*"+")(mail=*)(userAccountControl=512))";
NamingEnumeration answer = ctx.search("DC=xxx,DC=org", searchFilter, constraints);
if (answer != null) {
while (answer.hasMore()) {
attrs = ((SearchResult) answer.next()).getAttributes();
if (attrs.get("displayname") != null) {
int empNameLen = attrs.get("displayname").toString().length();
activeEmpAttributes.add(attrs.get("displayname").toString().substring(13, empNameLen));
}
count++;
ctx.close();
}
}
else {
throw new Exception("Invalid User");
}
System.out.println("activeEmpAttributes: " + activeEmpAttributes);
System.out.println("count: " + activeEmpAttributes.size());
} catch (Exception ex) {
ex.printStackTrace();
}
return activeEmpAttributes;
}
Thanks anyway.
I'm trying to use CLR integration in SQL Server to handle accessing external files instead of storing them internally as BLOBs. I'm trying to figure out the pattern I need to follow to make my code enlist in the current SQL transaction. I figured I would start with the simplest scenario, deleting an existing row, since the insert/update scenarios would be more complex.
[SqlProcedure]
public static void DeleteStoredImages(SqlInt64 DocumentID)
{
if (DocumentID.IsNull)
return;
using (var conn = new SqlConnection("context connection=true"))
{
conn.Open();
string FaceFileName, RearFileName;
int Offset, Length;
GetFileLocation(conn, DocumentID.Value, true,
out FaceFileName, out Offset, out Length);
GetFileLocation(conn, DocumentID.Value, false,
out RearFileName, out Offset, out Length);
new DeleteTransaction().Enlist(FaceFileName, RearFileName);
using (var comm = conn.CreateCommand())
{
comm.CommandText = "DELETE FROM ImagesStore WHERE DocumentID = " + DocumentID.Value;
comm.ExecuteNonQuery();
}
}
}
private class DeleteTransaction : IEnlistmentNotification
{
public string FaceFileName { get; set; }
public string RearFileName { get; set; }
public void Enlist(string FaceFileName, string RearFileName)
{
this.FaceFileName = FaceFileName;
this.RearFileName = RearFileName;
var trans = Transaction.Current;
if (trans == null)
Commit(null);
else
trans.EnlistVolatile(this, EnlistmentOptions.None);
}
public void Commit(Enlistment enlistment)
{
if (FaceFileName != null && File.Exists(FaceFileName))
{
File.Delete(FaceFileName);
}
if (RearFileName != null && File.Exists(RearFileName))
{
File.Delete(RearFileName);
}
}
public void InDoubt(Enlistment enlistment)
{
}
public void Prepare(PreparingEnlistment preparingEnlistment)
{
preparingEnlistment.Prepared();
}
public void Rollback(Enlistment enlistment)
{
}
}
When I actually try to run this, I get the following exception:
A .NET Framework error occurred during execution of user defined routine or aggregate 'DeleteStoredImages':
System.Transactions.TransactionException: The operation is not valid for the state of the transaction. ---> System.Transactions.TransactionPromotionException: MSDTC on server 'BD009' is unavailable. ---> System.Data.SqlClient.SqlException: MSDTC on server 'BD009' is unavailable.
System.Data.SqlClient.SqlException:
at System.Data.SqlServer.Internal.StandardEventSink.HandleErrors()
at System.Data.SqlServer.Internal.ClrLevelContext.SuperiorTransaction.Promote()
System.Transactions.TransactionPromotionException:
at System.Data.SqlServer.Internal.ClrLevelContext.SuperiorTransaction.Promote()
at System.Transactions.TransactionStatePSPEOperation.PSPEPromote(InternalTransaction tx)
at System.Transactions.TransactionStateDelegatedBase.EnterState(InternalTransaction tx)
System.Transactions.TransactionException:
at System.Transactions.TransactionState.EnlistVolatile(InternalTransaction tx, IEnlistmentNotification enlistmentNotification, EnlistmentOptions enlistmentOptions, Transaction atomicTransaction)
at System.Transactions.TransactionStateSubordinateActive.EnlistVolatile(InternalTransaction tx, IEnlistmentNotification enlistmentNotification, EnlistmentOptions enlistmentOptions, Transaction atomicTransaction)
at System.Transactions.Transaction.EnlistVolatile(IEnlistmentNotification enlistmentNotification, EnlistmentOptions enlistmentOptions)
at ExternalImages.StoredProcedures.DeleteTransaction.Enlist(String FaceFileName, String RearFileName)
at ExternalImages.StoredProcedures.DeleteStoredImages(SqlInt64 DocumentID)
. User transaction, if any, will be rolled back.
The statement has been terminated.
Can anyone explain what I'm doing wrong, or point me to an example of how to do it right?
You have hopefully solved this by now, but in case anyone else has a similar problem: the error message you are getting suggests that you need to start the Distributed Transaction Coordinator service on the BD009 machine (presumably your own machine).
#Aasmund's answer regarding the Distributed Transaction Coordinator might solve the stated problem, but that still leaves you in a non-ideal state: You are tying a transaction, which locks the ImagesStore table (even if it is just a RowLock), to two file system operations? And you need to BEGIN and COMMIT the transaction outside of this function (since that isn't being handled in the presented code).
I would separate those two pieces:
Step 1: Delete the row from the table
and then, IF that did not error,
Step 2: Delete the file(s)
In the scenario where Step 1 succeeds but then Step 2, for whatever reason, fails, do one or both of the following:
return an error status code and keep track of which DocumentIDs got an error when attempting to delete the file in a status table. You can use that to manually delete the files and/or debug why the error occurred.
create a process that can run periodically to find and remove unreferenced files.