Skip to content

Commit 024206d

Browse files
committed
Constraints validation fixes and improvements
- Fixed case when lazy load field fetched even if it has no constraint - Fixed but when validation tries to run on fields without constraints - Used late field value getting. Now validator gets value only when it is about to validate it. It is especially useful with lazy load fields.
1 parent cc35e26 commit 024206d

3 files changed

Lines changed: 226 additions & 7 deletions

File tree

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
// Copyright (C) 2020 Xtensive LLC.
2+
// All rights reserved.
3+
// For conditions of distribution and use, see license.
4+
// Created by: Alexey Kulakov
5+
// Created: 2020.02.18
6+
7+
using System;
8+
using System.Collections.Generic;
9+
using System.Text;
10+
using Xtensive.Orm.Configuration;
11+
using Xtensive.Orm.Validation;
12+
using Xtensive.Orm.Tests.Issues.IssueJira0793_FieldValidationTriggersLazyLoadFieldsFetchModel;
13+
using NUnit.Framework;
14+
using Xtensive.Core;
15+
16+
namespace Xtensive.Orm.Tests.Issues.IssueJira0793_FieldValidationTriggersLazyLoadFieldsFetchModel
17+
{
18+
[HierarchyRoot]
19+
public class Book : Entity
20+
{
21+
[Field, Key]
22+
public int Id { get; set; }
23+
24+
[Field(Length = 50), NotNullOrEmptyConstraint]
25+
public string Title { get; set; }
26+
27+
[Field(Length = int.MaxValue, LazyLoad = true)]
28+
public string Description { get; set; }
29+
30+
[Field(LazyLoad = true)]
31+
public byte[] BookFile { get; set; }
32+
33+
[Field(Length = 5)]
34+
public string FileExtension { get; set; }
35+
36+
[Field]
37+
public Author Author { get; set; }
38+
}
39+
40+
[HierarchyRoot]
41+
public class Author : Entity
42+
{
43+
[Field, Key]
44+
public int Id { get; set; }
45+
46+
[Field()]
47+
public string FirstName { get; set; }
48+
49+
[Field]
50+
public string LastName { get; set; }
51+
52+
[Field(Length = int.MaxValue, LazyLoad = true)]
53+
[NotNullConstraint]// should always be triggered
54+
public string Biography { get; set; }
55+
}
56+
57+
[HierarchyRoot]
58+
public class Chapter : Entity
59+
{
60+
[Field, Key]
61+
public int Id { get; set; }
62+
63+
[Field]
64+
public Book Owner { get; set; }
65+
66+
[Field]
67+
public string Title { get; set; }
68+
69+
[Field(Length = int.MaxValue, LazyLoad = true)]
70+
[NotNullOrEmptyConstraint(ValidateOnlyIfModified = true)]// should validate only if changed
71+
public string Description { get; set; }
72+
}
73+
}
74+
75+
namespace Xtensive.Orm.Tests.Issues
76+
{
77+
public class IssueJira0793_FieldValidationTriggersLazyLoadFieldsFetch : AutoBuildTest
78+
{
79+
private class QueryCounter
80+
{
81+
public int Count { get; private set; }
82+
83+
public Disposable Attach(Session session)
84+
{
85+
session.Events.DbCommandExecuted += Encount;
86+
return new Disposable((disposing) => session.Events.DbCommandExecuted -= Encount);
87+
}
88+
89+
public void Reset()
90+
{
91+
Count = 0;
92+
}
93+
94+
private void Encount(object sender, DbCommandEventArgs eventArgs)
95+
{
96+
Count++;
97+
}
98+
99+
public QueryCounter()
100+
{
101+
Count = 0;
102+
}
103+
}
104+
105+
private Key oblomovKey;
106+
private Key goncharovKey;
107+
108+
protected override DomainConfiguration BuildConfiguration()
109+
{
110+
var configuration = base.BuildConfiguration();
111+
configuration.Types.Register(typeof (Book).Assembly, typeof (Book).Namespace);
112+
configuration.UpgradeMode = DomainUpgradeMode.Recreate;
113+
return configuration;
114+
}
115+
116+
protected override void PopulateData()
117+
{
118+
using (var session = Domain.OpenSession())
119+
using (var transaction = session.OpenTransaction()) {
120+
var author = new Author() { FirstName = "Ivan", LastName = "Goncharov", Biography = "Some biography of Ivan Alexandrovich" };
121+
var book = new Book() {
122+
Title = "Oblomov",
123+
Description = "A drama story of how human lazyness and absence of strenght within may cause his life.",
124+
BookFile = new byte[] { 3, 3, 3, 3, 3, 3, 3 },
125+
Author = author
126+
};
127+
128+
new Chapter() { Title = "Chapter #1", Description = "Detailed description of Chapter #1", Owner = book };
129+
new Chapter() { Title = "Chapter #2", Description = "Detailed description of Chapter #2", Owner = book };
130+
new Chapter() { Title = "Chapter #3", Description = "Detailed description of Chapter #3", Owner = book };
131+
132+
oblomovKey = book.Key;
133+
goncharovKey = author.Key;
134+
135+
transaction.Complete();
136+
}
137+
}
138+
139+
[Test]
140+
public void LazyFieldHasNoConstraintTest()
141+
{
142+
using (var session = Domain.OpenSession()) {
143+
var counter = new QueryCounter();
144+
using (counter.Attach(session)) {
145+
using (var transaction = session.OpenTransaction()) {
146+
var oblomov = session.Query.Single<Book>(oblomovKey);
147+
oblomov.Title = "Oblomov by Goncharov";
148+
transaction.Complete();
149+
}
150+
}
151+
Assert.That(counter.Count, Is.EqualTo(2));
152+
counter.Reset();
153+
}
154+
}
155+
156+
[Test]
157+
public void LazyFieldHasCheckAlwaysConstraintTest()
158+
{
159+
using (var session = Domain.OpenSession()) {
160+
var counter = new QueryCounter();
161+
using (counter.Attach(session)) {
162+
using (var transaction = session.OpenTransaction()) {
163+
var goncharov = session.Query.Single<Author>(goncharovKey);
164+
goncharov.FirstName = goncharov.FirstName + "modified"; // should prefetch lazy load field
165+
transaction.Complete();
166+
}
167+
}
168+
Assert.That(counter.Count, Is.EqualTo(3));
169+
counter.Reset();
170+
}
171+
}
172+
173+
[Test]
174+
public void LazyFieldHasCheckIfModifiedConstraintTest()
175+
{
176+
using (var session = Domain.OpenSession()) {
177+
var counter = new QueryCounter();
178+
using (counter.Attach(session)) {
179+
using (var transaction = session.OpenTransaction()) {
180+
foreach (var chapter in session.Query.All<Chapter>()) {
181+
chapter.Title = chapter.Title + " modified";
182+
}
183+
transaction.Complete();
184+
}
185+
Assert.That(counter.Count, Is.EqualTo(2));
186+
counter.Reset();
187+
}
188+
}
189+
190+
using (var session = Domain.OpenSession()) {
191+
var counter = new QueryCounter();
192+
using (counter.Attach(session)) {
193+
int updatedItems = 0;
194+
using (var transaction = session.OpenTransaction()) {
195+
foreach(var chapter in session.Query.All<Chapter>()) {
196+
chapter.Description = chapter.Description + " modified";
197+
updatedItems++;
198+
}
199+
transaction.Complete();
200+
}
201+
Assert.That(counter.Count, Is.EqualTo(5)); // query + fetches + update
202+
counter.Reset();
203+
}
204+
}
205+
}
206+
}
207+
}

Orm/Xtensive.Orm.Tests/Xtensive.Orm.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,7 @@
349349
<Compile Include="Issues\IssueJira0735_WrongTypeIdsInValidateMode.cs" />
350350
<Compile Include="Issues\IssueJira0779_SetOperationsWithOrderAndTakeWrongQuery.cs" />
351351
<Compile Include="Issues\IssueJira0792_UnableToRemoveAssignedEntityWithNonNullableAssociationField.cs" />
352+
<Compile Include="Issues\IssueJira0793_FieldValidationTriggersLazyLoadFieldsFetch.cs" />
352353
<Compile Include="Issues\IssueJira_0530_LowSpeedDecimalMaterializationCouseSqlDecimalInternalException.cs" />
353354
<Compile Include="Issues\Issue_0694_SchemaUpgradeBug\ModelVersion1.cs" />
354355
<Compile Include="Issues\Issue_0694_SchemaUpgradeBug\ModelVersion2.cs" />

Orm/Xtensive.Orm/Orm/Validation/Internals/RealValidationContext.cs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -148,19 +148,23 @@ private void GetValidationErrors(List<EntityErrorInfo> output, ValidationReason?
148148
private void GetValidationErrors(Entity target, List<ValidationResult> output, ValidationReason? validationReason = null)
149149
{
150150
foreach (var field in target.TypeInfo.Fields) {
151-
var value = target.GetFieldValue(field);
151+
if (!field.HasValidators)
152+
continue;
153+
154+
object value = null;
155+
bool isValueRetrieved = false;
152156
foreach (var validator in field.Validators) {
153157
if (validationReason.HasValue && validationReason.Value==ValidationReason.Commit && validator.SkipOnTransactionCommit)
154158
continue;
155159
if (validator.ValidateOnlyIfModified) {
156-
if(changedFields==null)
157-
continue;
158-
HashSet<FieldInfo> fieldSet;
159-
if (!changedFields.TryGetValue(target, out fieldSet))
160-
continue;
161-
if(!fieldSet.Contains(field))
160+
if (ShouldSkipModifiedOnlyValidator(target, field))
162161
continue;
163162
}
163+
if (!isValueRetrieved) {
164+
value = target.GetFieldValue(field);
165+
isValueRetrieved = true;
166+
}
167+
164168
var result = validator.Validate(target, value);
165169
if (result.IsError)
166170
output.Add(result);
@@ -185,5 +189,12 @@ private string GetErrorMessage(ValidationReason reason)
185189
throw new ArgumentOutOfRangeException("reason");
186190
}
187191
}
192+
193+
private bool ShouldSkipModifiedOnlyValidator(Entity target, FieldInfo field)
194+
{
195+
return changedFields==null ||
196+
!changedFields.TryGetValue(target, out var fieldSet) ||
197+
!fieldSet.Contains(field);
198+
}
188199
}
189200
}

0 commit comments

Comments
 (0)