您的位置:首页 > 其它

使用EF6和MVC5实现一个简单的选课系统--使用EF6处理并发操作(10/12)

2014-03-24 20:59 876 查看
先贴出来英文的,抽空翻译!

本讲源代码下载

TheContosoUniversitysamplewebapplicationdemonstrateshowtocreateASP.NETMVC5applicationsusingtheEntityFramework6CodeFirstandVisualStudio2013.Forinformationaboutthetutorialseries,seethe
firsttutorialintheseries.

Inearliertutorialsyoulearnedhowtoupdatedata.Thistutorialshowshowtohandleconflictswhenmultipleusersupdatethesameentityatthesametime.

You'llchangethewebpagesthatworkwiththe
Department
entity
sothattheyhandleconcurrencyerrors.ThefollowingillustrationsshowtheIndexandDeletepages,includingsomemessagesthataredisplayedifaconcurrencyconflictoccurs.






ConcurrencyConflicts

Aconcurrencyconflictoccurswhenoneuserdisplaysanentity'sdatainordertoeditit,andthenanotheruserupdatesthesameentity'sdatabeforethefirstuser'schangeiswrittentothedatabase.Ifyoudon'tenablethedetectionofsuchconflicts,whoever
updatesthedatabaselastoverwritestheotheruser'schanges.Inmanyapplications,thisriskisacceptable:iftherearefewusers,orfewupdates,orifisn'treallycriticalifsomechangesareoverwritten,thecostofprogrammingforconcurrencymight
outweighthebenefit.Inthatcase,youdon'thavetoconfiguretheapplicationtohandleconcurrencyconflicts.


PessimisticConcurrency(Locking)

Ifyourapplicationdoesneedtopreventaccidentaldatalossinconcurrencyscenarios,onewaytodothatistousedatabaselocks.Thisiscalledpessimistic
concurrency.Forexample,beforeyoureadarowfromadatabase,yourequestalockforread-onlyorforupdateaccess.Ifyoulockarowforupdateaccess,nootherusersareallowedtolocktheroweitherforread-onlyorupdateaccess,becausethey
wouldgetacopyofdatathat'sintheprocessofbeingchanged.Ifyoulockarowforread-onlyaccess,otherscanalsolockitforread-onlyaccessbutnotforupdate.

Managinglockshasdisadvantages.Itcanbecomplextoprogram.Itrequiressignificantdatabasemanagementresources,anditcancauseperformanceproblemsasthenumberofusersofanapplicationincreases.Forthesereasons,notalldatabasemanagementsystems
supportpessimisticconcurrency.TheEntityFrameworkprovidesnobuilt-insupportforit,andthistutorialdoesn'tshowyouhowtoimplementit.


OptimisticConcurrency

Thealternativetopessimisticconcurrencyisoptimisticconcurrency.Optimisticconcurrencymeansallowingconcurrency
conflictstohappen,andthenreactingappropriatelyiftheydo.Forexample,JohnrunstheDepartmentsEditpage,changestheBudgetamount
fortheEnglishdepartmentfrom$350,000.00to$0.00.



BeforeJohnclicksSave,JanerunsthesamepageandchangestheStart
Datefieldfrom9/1/2007to8/8/2013.



JohnclicksSavefirstandseeshischangewhenthebrowserreturnstotheIndexpage,thenJaneclicksSave.
Whathappensnextisdeterminedbyhowyouhandleconcurrencyconflicts.Someoftheoptionsincludethefollowing:

Youcankeeptrackofwhichpropertyauserhasmodifiedandupdateonlythecorrespondingcolumnsinthedatabase.Intheexamplescenario,nodatawouldbelost,becausedifferentpropertieswereupdatedbythetwousers.Thenexttimesomeonebrowsesthe
Englishdepartment,they'llseebothJohn'sandJane'schanges—astartdateof8/8/2013andabudgetofZerodollars.

Thismethodofupdatingcanreducethenumberofconflictsthatcouldresultindataloss,butitcan'tavoiddatalossifcompetingchangesaremadetothesamepropertyofanentity.WhethertheEntityFrameworkworksthiswaydependsonhowyouimplement
yourupdatecode.It'softennotpracticalinawebapplication,becauseitcanrequirethatyoumaintainlargeamountsofstateinordertokeeptrackofalloriginalpropertyvaluesforanentityaswellasnewvalues.Maintaininglargeamountsofstate
canaffectapplicationperformancebecauseiteitherrequiresserverresourcesormustbeincludedinthewebpageitself(forexample,inhiddenfields)orinacookie.

YoucanletJane'schangeoverwriteJohn'schange.ThenexttimesomeonebrowsestheEnglishdepartment,they'llsee8/8/2013andtherestored$350,000.00value.ThisiscalledaClient
WinsorLastinWinsscenario.(Allvaluesfromtheclienttakeprecedenceoverwhat'sinthedatastore.)As
notedintheintroductiontothissection,ifyoudon'tdoanycodingforconcurrencyhandling,thiswillhappenautomatically.

YoucanpreventJane'schangefrombeingupdatedinthedatabase.Typically,youwoulddisplayanerrormessage,showherthecurrentstateofthedata,andallowhertoreapplyherchangesifshestillwantstomakethem.ThisiscalledaStore
Winsscenario.(Thedata-storevaluestakeprecedenceoverthevaluessubmittedbytheclient.)You'llimplementtheStoreWinsscenariointhistutorial.Thismethodensuresthatnochangesareoverwrittenwithoutauserbeingalertedtowhat'shappening.


DetectingConcurrencyConflicts

YoucanresolveconflictsbyhandlingOptimisticConcurrencyExceptionexceptions
thattheEntityFrameworkthrows.Inordertoknowwhentothrowtheseexceptions,theEntityFrameworkmustbeabletodetectconflicts.Therefore,youmustconfigurethedatabaseandthedatamodelappropriately.Someoptionsforenablingconflictdetection
includethefollowing:

Inthedatabasetable,includeatrackingcolumnthatcanbeusedtodeterminewhenarowhasbeenchanged.YoucanthenconfiguretheEntityFrameworktoincludethatcolumninthe
Where
clause
ofSQL
Update
or
Delete
commands.

Thedatatypeofthetrackingcolumnistypicallyrowversion
.
Therowversionvalue
isasequentialnumberthat'sincrementedeachtimetherowisupdated.Inan
Update
or
Delete
command,
the
Where
clauseincludestheoriginalvalueofthetrackingcolumn
(theoriginalrowversion).Iftherowbeingupdatedhasbeenchangedbyanotheruser,thevalueinthe
rowversion
column
isdifferentthantheoriginalvalue,sothe
Update
or
Delete
statement
can'tfindtherowtoupdatebecauseofthe
Where
clause.When
theEntityFrameworkfindsthatnorowshavebeenupdatedbythe
Update
or
Delete
command
(thatis,whenthenumberofaffectedrowsiszero),itinterpretsthatasaconcurrencyconflict.

ConfiguretheEntityFrameworktoincludetheoriginalvaluesofeverycolumninthetableinthe
Where
clause
of
Update
and
Delete
commands.

Asinthefirstoption,ifanythingintherowhaschangedsincetherowwasfirstread,the
Where
clause
won'treturnarowtoupdate,whichtheEntityFrameworkinterpretsasaconcurrencyconflict.Fordatabasetablesthathavemanycolumns,thisapproachcanresultinverylarge
Where
clauses,
andcanrequirethatyoumaintainlargeamountsofstate.Asnotedearlier,maintaininglargeamountsofstatecanaffectapplicationperformance.Thereforethisapproachisgenerallynotrecommended,anditisn'tthemethodusedinthistutorial.

Ifyoudowanttoimplementthisapproachtoconcurrency,youhavetomarkallnon-primary-keypropertiesintheentityyouwanttotrackconcurrencyforbyaddingtheConcurrencyCheckattribute
tothem.ThatchangeenablestheEntityFrameworktoincludeallcolumnsintheSQL
WHERE
clause
of
UPDATE
statements.

Intheremainderofthistutorialyou'lladdarowversiontracking
propertytothe
Department
entity,createacontrollerandviews,
andtesttoverifythateverythingworkscorrectly.


AddanOptimisticConcurrencyPropertytotheDepartmentEntity

InModels\Department.cs,addatrackingpropertynamed
RowVersion
:
publicclassDepartment
{
publicintDepartmentID{get;set;}

[StringLength(50,MinimumLength=3)]
publicstringName{get;set;}

[DataType(DataType.Currency)]
[Column(TypeName="money")]
publicdecimalBudget{get;set;}

[DataType(DataType.Date)]
[DisplayFormat(DataFormatString="{0:yyyy-MM-dd}",ApplyFormatInEditMode=true)]
[Display(Name="StartDate")]
publicDateTimeStartDate{get;set;}

[Display(Name="Administrator")]
publicint?InstructorID{get;set;}

[Timestamp]
publicbyte[]RowVersion{get;set;}

publicvirtualInstructorAdministrator{get;set;}
publicvirtualICollection<Course>Courses{get;set;}
}


TheTimestampattribute
specifiesthatthiscolumnwillbeincludedinthe
Where
clause
of
Update
and
Delete
commands
senttothedatabase.TheattributeiscalledTimestampbecause
previousversionsofSQLServerusedaSQLtimestampdata
typebeforetheSQLrowversionreplaced
it.The.Nettypefor
rowversionis
abytearray.

IfyouprefertousethefluentAPI,youcanusetheIsConcurrencyTokenmethod
tospecifythetrackingproperty,asshowninthefollowingexample:
modelBuilder.Entity<Department>()
.Property(p=>p.RowVersion).IsConcurrencyToken();


Byaddingapropertyyouchangedthedatabasemodel,soyouneedtodoanothermigration.InthePackageManagerConsole(PMC),enterthefollowingcommands:

Add-MigrationRowVersion

Update-Database


ModifytheDepartmentController

InControllers\DepartmentController.cs,adda
using
statement:
usingSystem.Data.Entity.Infrastructure;


IntheDepartmentController.csfile,changeallfouroccurrencesof"LastName"to"FullName"sothatthedepartment
administratordrop-downlistswillcontainthefullnameoftheinstructorratherthanjustthelastname.
ViewBag.InstructorID=newSelectList(db.Instructors,"InstructorID","FullName");


Replacetheexistingcodeforthe
HttpPost
Edit
method
withthefollowingcode:
[HttpPost]
[ValidateAntiForgeryToken]
publicasyncTask<ActionResult>Edit(
[Bind(Include="DepartmentID,Name,Budget,StartDate,RowVersion,InstructorID")]
Departmentdepartment)
{
try
{
if(ModelState.IsValid)
{
db.Entry(department).State=EntityState.Modified;
awaitdb.SaveChangesAsync();
returnRedirectToAction("Index");
}
}
catch(DbUpdateConcurrencyExceptionex)
{
varentry=ex.Entries.Single();
varclientValues=(Department)entry.Entity;
vardatabaseEntry=entry.GetDatabaseValues();
if(databaseEntry==null)
{
ModelState.AddModelError(string.Empty,
"Unabletosavechanges.Thedepartmentwasdeletedbyanotheruser.");
}
else
{
vardatabaseValues=(Department)databaseEntry.ToObject();

if(databaseValues.Name!=clientValues.Name)
ModelState.AddModelError("Name","Currentvalue:"
+databaseValues.Name);
if(databaseValues.Budget!=clientValues.Budget)
ModelState.AddModelError("Budget","Currentvalue:"
+String.Format("{0:c}",databaseValues.Budget));
if(databaseValues.StartDate!=clientValues.StartDate)
ModelState.AddModelError("StartDate","Currentvalue:"
+String.Format("{0:d}",databaseValues.StartDate));
if(databaseValues.InstructorID!=clientValues.InstructorID)
ModelState.AddModelError("InstructorID","Currentvalue:"
+db.Instructors.Find(databaseValues.InstructorID).FullName);
ModelState.AddModelError(string.Empty,"Therecordyouattemptedtoedit"
+"wasmodifiedbyanotheruserafteryougottheoriginalvalue.The"
+"editoperationwascanceledandthecurrentvaluesinthedatabase"
+"havebeendisplayed.Ifyoustillwanttoeditthisrecord,click"
+"theSavebuttonagain.OtherwiseclicktheBacktoListhyperlink.");
department.RowVersion=databaseValues.RowVersion;
}
}
catch(RetryLimitExceededException/*dex*/)
{
//Logtheerror(uncommentdexvariablenameandaddalineheretowritealog.
ModelState.AddModelError(string.Empty,"Unabletosavechanges.Tryagain,andiftheproblempersistscontactyoursystemadministrator.");
}

ViewBag.InstructorID=newSelectList(db.Instructors,"ID","FullName",department.InstructorID);
returnView(department);
}


Theviewwillstoretheoriginal
RowVersion
valueinahiddenfield.
Whenthemodelbindercreatesthe
department
instance,thatobject
willhavetheoriginal
RowVersion
propertyvalueandthenewvalues
fortheotherproperties,asenteredbytheuserontheEditpage.ThenwhentheEntityFrameworkcreatesaSQL
UPDATE
command,
thatcommandwillincludea
WHERE
clausethatlooksforarow
thathastheoriginal
RowVersion
value.

Ifnorowsareaffectedbythe
UPDATE
command(norowshavethe
original
RowVersion
value),theEntityFrameworkthrowsa
DbUpdateConcurrencyException
exception,
andthecodeinthe
catch
blockgetstheaffected
Department
entity
fromtheexceptionobject.
varentry=ex.Entries.Single();


Thisobjecthasthenewvaluesenteredbytheuserinits
Entity
property,
andyoucangetthevaluesreadfromthedatabasebycallingthe
GetDatabaseValues
method.
varclientValues=(Department)entry.Entity;
vardatabaseEntry=entry.GetDatabaseValues();


The
GetDatabaseValues
methodreturnsnullifsomeonehasdeleted
therowfromthedatabase;otherwise,youhavetocasttheobjectreturnedtothe
Department
class
inordertoaccessthe
Department
properties.
if(databaseEntry==null)
{
ModelState.AddModelError(string.Empty,
"Unabletosavechanges.Thedepartmentwasdeletedbyanotheruser.");
}
else
{
vardatabaseValues=(Department)databaseEntry.ToObject();


Next,thecodeaddsacustomerrormessageforeachcolumnthathasdatabasevaluesdifferentfromwhattheuserenteredontheEditpage:
if(databaseValues.Name!=currentValues.Name)
ModelState.AddModelError("Name","Currentvalue:"+databaseValues.Name);
//...


Alongererrormessageexplainswhathappenedandwhattodoaboutit:
ModelState.AddModelError(string.Empty,"Therecordyouattemptedtoedit"
+"wasmodifiedbyanotheruserafteryougottheoriginalvalue.The"
+"editoperationwascanceledandthecurrentvaluesinthedatabase"
+"havebeendisplayed.Ifyoustillwanttoeditthisrecord,click"
+"theSavebuttonagain.OtherwiseclicktheBacktoListhyperlink.");


Finally,thecodesetsthe
RowVersion
valueofthe
Department
object
tothenewvalueretrievedfromthedatabase.Thisnew
RowVersion
value
willbestoredinthehiddenfieldwhentheEditpageisredisplayed,andthenexttimetheuserclicksSave,only
concurrencyerrorsthathappensincetheredisplayoftheEditpagewillbecaught.

InViews\Department\Edit.cshtml,addahiddenfieldtosavethe
RowVersion
property
value,immediatelyfollowingthehiddenfieldforthe
DepartmentID
property:
@modelContosoUniversity.Models.Department

@{
ViewBag.Title="Edit";
}

<h2>Edit</h2>

@using(Html.BeginForm())
{
@Html.AntiForgeryToken()

<divclass="form-horizontal">
<h4>Department</h4>
<hr/>
@Html.ValidationSummary(true)
@Html.HiddenFor(model=>model.DepartmentID)
@Html.HiddenFor(model=>model.RowVersion)


TestingOptimisticConcurrencyHandling

RunthesiteandclickDepartments:



RightclicktheEdithyperlinkfortheEnglishdepartmentandselectOpen
innewtab,thenclicktheEdithyperlinkfortheEnglishdepartment.Thetwotabsdisplaythesameinformation.



ChangeafieldinthefirstbrowsertabandclickSave.



ThebrowsershowstheIndexpagewiththechangedvalue.



ChangeafieldinthesecondbrowsertabandclickSave.



ClickSaveinthesecondbrowsertab.Youseeanerrormessage:



ClickSaveagain.Thevalueyouenteredinthesecondbrowsertabissavedalongwiththeoriginalvalueofthedata
youchangedinthefirstbrowser.YouseethesavedvalueswhentheIndexpageappears.




UpdatingtheDeletePage

FortheDeletepage,theEntityFrameworkdetectsconcurrencyconflictscausedbysomeoneelseeditingthedepartmentinasimilarmanner.Whenthe
HttpGet
Delete
method
displaystheconfirmationview,theviewincludestheoriginal
RowVersion
value
inahiddenfield.Thatvalueisthenavailabletothe
HttpPost
Delete
method
that'scalledwhentheuserconfirmsthedeletion.WhentheEntityFrameworkcreatestheSQL
DELETE
command,
itincludesa
WHERE
clausewiththeoriginal
RowVersion
value.
Ifthecommandresultsinzerorowsaffected(meaningtherowwaschangedaftertheDeleteconfirmationpagewasdisplayed),aconcurrencyexceptionisthrown,andthe
HttpGet
Delete
methodiscalledwithanerrorflagsetto
true
in
ordertoredisplaytheconfirmationpagewithanerrormessage.It'salsopossiblethatzerorowswereaffectedbecausetherowwasdeletedbyanotheruser,sointhatcaseadifferenterrormessageisdisplayed.

InDepartmentController.cs,replacethe
HttpGet
Delete
method
withthefollowingcode:
publicActionResultDelete(int?id,bool?concurrencyError)
{
if(id==null)
{
returnnewHttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Departmentdepartment=db.Departments.Find(id);
if(department==null)
{
returnHttpNotFound();
}

if(concurrencyError.GetValueOrDefault())
{
if(department==null)
{
ViewBag.ConcurrencyErrorMessage="Therecordyouattemptedtodelete"
+"wasdeletedbyanotheruserafteryougottheoriginalvalues."
+"ClicktheBacktoListhyperlink.";
}
else
{
ViewBag.ConcurrencyErrorMessage="Therecordyouattemptedtodelete"
+"wasmodifiedbyanotheruserafteryougottheoriginalvalues."
+"Thedeleteoperationwascanceledandthecurrentvaluesinthe"
+"databasehavebeendisplayed.Ifyoustillwanttodeletethis"
+"record,clicktheDeletebuttonagain.Otherwise"
+"clicktheBacktoListhyperlink.";
}
}

returnView(department);
}


Themethodacceptsanoptionalparameterthatindicateswhetherthepageisbeingredisplayedafteraconcurrencyerror.Ifthisflagis
true
,
anerrormessageissenttotheviewusinga
ViewBag
property.

Replacethecodeinthe
HttpPost
Delete
method
(named
DeleteConfirmed
)withthefollowingcode:
[HttpPost]
[ValidateAntiForgeryToken]
publicActionResultDelete(Departmentdepartment)
{
try
{
db.Entry(department).State=EntityState.Deleted;
db.SaveChanges();
returnRedirectToAction("Index");
}
catch(DbUpdateConcurrencyException)
{
returnRedirectToAction("Delete",new{concurrencyError=true});
}
catch(DataException/*dex*/)
{
//Logtheerror(uncommentdexvariablenameafterDataExceptionandaddalineheretowritealog.
ModelState.AddModelError(string.Empty,"Unabletodelete.Tryagain,andiftheproblempersistscontactyoursystemadministrator.");
returnView(department);
}
}


Inthescaffoldedcodethatyoujustreplaced,thismethodacceptedonlyarecordID:
publicActionResultDeleteConfirmed(intid)


You'vechangedthisparametertoa
Department
entityinstancecreated
bythemodelbinder.Thisgivesyouaccesstothe
RowVersion
property
valueinadditiontotherecordkey.
publicActionResultDelete(Departmentdepartment)


Youhavealsochangedtheactionmethodnamefrom
DeleteConfirmed
to
Delete
.
Thescaffoldedcodenamedthe
HttpPost
Delete
method
DeleteConfirmed
to
givethe
HttpPost
methodauniquesignature.(TheCLRrequires
overloadedmethodstohavedifferentmethodparameters.)Nowthatthesignaturesareunique,youcanstickwiththeMVCconventionandusethesamenameforthe
HttpPost
and
HttpGet
delete
methods.

Ifaconcurrencyerroriscaught,thecoderedisplaystheDeleteconfirmationpageandprovidesaflagthatindicatesitshoulddisplayaconcurrencyerrormessage.

InViews\Department\Delete.cshtml,replacethescaffoldedcodewiththefollowingcodethataddsanerrormessagefield
andhiddenfieldsfortheDepartmentIDandRowVersionproperties.Thechangesarehighlighted.
@modelContosoUniversity.Models.Department

@{
ViewBag.Title="Delete";
}

<h2>Delete</h2>

<pclass="error">@ViewBag.ConcurrencyErrorMessage</p>

<h3>Areyousureyouwanttodeletethis?</h3>
<div>
<h4>Department</h4>
<hr/>
<dlclass="dl-horizontal">
<dt>
Administrator
</dt>

<dd>
@Html.DisplayFor(model=>model.Administrator.FullName)
</dd>

<dt>
@Html.DisplayNameFor(model=>model.Name)
</dt>

<dd>
@Html.DisplayFor(model=>model.Name)
</dd>

<dt>
@Html.DisplayNameFor(model=>model.Budget)
</dt>

<dd>
@Html.DisplayFor(model=>model.Budget)
</dd>

<dt>
@Html.DisplayNameFor(model=>model.StartDate)
</dt>

<dd>
@Html.DisplayFor(model=>model.StartDate)
</dd>

</dl>

@using(Html.BeginForm()){
@Html.AntiForgeryToken()
@Html.HiddenFor(model=>model.DepartmentID)
@Html.HiddenFor(model=>model.RowVersion)

<divclass="form-actionsno-color">
<inputtype="submit"value="Delete"class="btnbtn-default"/>|
@Html.ActionLink("BacktoList","Index")
</div>
}
</div>


Thiscodeaddsanerrormessagebetweenthe
h2
and
h3
headings:
<pclass="error">@ViewBag.ConcurrencyErrorMessage</p>


Itreplaces
LastName
with
FullName
in
the
Administrator
field:
<dt>
Administrator
</dt>
<dd>
@Html.DisplayFor(model=>model.Administrator.FullName)
</dd>


Finally,itaddshiddenfieldsforthe
DepartmentID
and
RowVersion
properties
afterthe
Html.BeginForm
statement:
@Html.HiddenFor(model=>model.DepartmentID)
@Html.HiddenFor(model=>model.RowVersion)


RuntheDepartmentsIndexpage.RightclicktheDeletehyperlinkfortheEnglishdepartmentandselectOpen
innewtab,theninthefirsttabclicktheEdithyperlinkfortheEnglishdepartment.

Inthefirstwindow,changeoneofthevalues,andclickSave:



TheIndexpageconfirmsthechange.



Inthesecondtab,clickDelete.



Youseetheconcurrencyerrormessage,andtheDepartmentvaluesarerefreshedwithwhat'scurrentlyinthedatabase.



IfyouclickDeleteagain,you'reredirectedtotheIndexpage,whichshowsthatthedepartmenthasbeendeleted.


Summary

Thiscompletestheintroductiontohandlingconcurrencyconflicts.Forinformationaboutotherwaystohandlevariousconcurrencyscenarios,seeOptimistic
ConcurrencyPatternsandWorking
withPropertyValuesonMSDN.Thenexttutorialshowshowtoimplementtable-per-hierarchyinheritanceforthe
Instructor
and
Student
entities.

LinkstootherEntityFrameworkresourcescanbefoundintheASP.NET
DataAccess-RecommendedResources.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐