-
Notifications
You must be signed in to change notification settings - Fork 85
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge remote-tracking branch 'tfs/liteModel'
- Loading branch information
Showing
146 changed files
with
2,566 additions
and
1,390 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,257 @@ | ||
using Signum.Utilities.Reflection; | ||
using Signum.Engine.Linq; | ||
using System.Data; | ||
using Signum.Entities.Internal; | ||
using Signum.Engine.Connection; | ||
|
||
namespace Signum.Engine.Cache; | ||
|
||
class CachedLiteTable<T> : CachedTableBase where T : Entity | ||
{ | ||
public override IColumn? ParentColumn { get; set; } | ||
|
||
Table table; | ||
|
||
Alias currentAlias; | ||
string lastPartialJoin; | ||
string? remainingJoins; | ||
|
||
Func<FieldReader, object> rowReader = null!; | ||
ResetLazy<Dictionary<PrimaryKey, object>> rows = null!; | ||
Func<object, PrimaryKey> idGetter; | ||
Dictionary<Type, ICachedLiteModelConstructor> liteModelConstructors = null!; | ||
|
||
SemiCachedController<T>? semiCachedController; | ||
|
||
public Dictionary<PrimaryKey, object> GetRows() | ||
{ | ||
return rows.Value; | ||
} | ||
|
||
public CachedLiteTable(ICacheLogicController controller, AliasGenerator aliasGenerator, string lastPartialJoin, string? remainingJoins) | ||
: base(controller) | ||
{ | ||
this.table = Schema.Current.Table(typeof(T)); | ||
|
||
HashSet<IColumn> columns = new HashSet<IColumn> { table.PrimaryKey }; | ||
|
||
foreach (var modelType in Lite.GetAllLiteModelTypes(typeof(T))) | ||
{ | ||
var modelConstructor = Lite.GetModelConstructorExpression(typeof(T), modelType); | ||
|
||
ToStringColumnsFinderVisitor.GatherColumns(modelConstructor, this.table, columns); | ||
} | ||
|
||
var ctr = this.Constructor = new CachedTableConstructor(this, aliasGenerator, columns.ToList()); | ||
|
||
this.lastPartialJoin = lastPartialJoin; | ||
this.remainingJoins = remainingJoins; | ||
this.currentAlias = aliasGenerator.NextTableAlias(table.Name.Name); | ||
var isPostgres = Schema.Current.Settings.IsPostgres; | ||
|
||
//Query | ||
using (ObjectName.OverrideOptions(new ObjectNameOptions { AvoidDatabaseName = true })) | ||
{ | ||
string select = "SELECT {0}\r\nFROM {1} {2}\r\n".FormatWith( | ||
ctr.columns.ToString(c => currentAlias + "." + c.Name.SqlEscape(isPostgres), ", "), | ||
table.Name.ToString(), | ||
currentAlias.ToString()); | ||
|
||
select += this.lastPartialJoin + currentAlias + "." + table.PrimaryKey.Name.SqlEscape(isPostgres) + "\r\n" + this.remainingJoins; | ||
|
||
query = new SqlPreCommandSimple(select); | ||
} | ||
|
||
//Reader | ||
{ | ||
rowReader = ctr.GetRowReader(); | ||
|
||
idGetter = ctr.GetPrimaryKeyGetter((IColumn)table.PrimaryKey); | ||
} | ||
|
||
rows = new ResetLazy<Dictionary<PrimaryKey, object>>(() => | ||
{ | ||
return SqlServerRetry.Retry(() => | ||
{ | ||
CacheLogic.AssertSqlDependencyStarted(); | ||
|
||
Dictionary<PrimaryKey, object> result = new Dictionary<PrimaryKey, object>(); | ||
|
||
using (MeasureLoad()) | ||
using (Connector.Override(Connector.Current.ForDatabase(table.Name.Schema?.Database))) | ||
using (var tr = Transaction.ForceNew(IsolationLevel.ReadCommitted)) | ||
{ | ||
if (CacheLogic.LogWriter != null) | ||
CacheLogic.LogWriter.WriteLine("Load {0}".FormatWith(GetType().TypeName())); | ||
|
||
Connector.Current.ExecuteDataReaderOptionalDependency(query, OnChange, fr => | ||
{ | ||
var obj = rowReader(fr); | ||
result[idGetter(obj)] = obj; //Could be repeated joins | ||
}); | ||
tr.Commit(); | ||
} | ||
|
||
return result; | ||
}); | ||
}, mode: LazyThreadSafetyMode.ExecutionAndPublication); | ||
|
||
if (!CacheLogic.WithSqlDependency) //Always semi | ||
{ | ||
semiCachedController = new SemiCachedController<T>(this); | ||
} | ||
} | ||
|
||
public override void SchemaCompleted() | ||
{ | ||
this.liteModelConstructors = Lite.GetAllLiteModelTypes(typeof(T)) | ||
.ToDictionary(modelType => modelType, modelType => | ||
{ | ||
var modelConstructor = Lite.GetModelConstructorExpression(typeof(T), modelType); | ||
var cachedModelConstructor = LiteModelExpressionVisitor.giGetCachedLiteModelConstructor.GetInvoker(typeof(T), modelType)(this.Constructor, modelConstructor); | ||
return cachedModelConstructor; | ||
}); | ||
|
||
if (this.subTables != null) | ||
foreach (var item in this.subTables) | ||
item.SchemaCompleted(); | ||
} | ||
|
||
protected override void Reset() | ||
{ | ||
if (rows == null) | ||
return; | ||
|
||
if (CacheLogic.LogWriter != null ) | ||
CacheLogic.LogWriter.WriteLine((rows.IsValueCreated ? "RESET {0}" : "Reset {0}").FormatWith(GetType().TypeName())); | ||
|
||
rows.Reset(); | ||
} | ||
|
||
protected override void Load() | ||
{ | ||
if (rows == null) | ||
return; | ||
|
||
rows.Load(); | ||
} | ||
|
||
|
||
public Lite<T> GetLite(PrimaryKey id, IRetriever retriever, Type modelType) | ||
{ | ||
Interlocked.Increment(ref hits); | ||
|
||
var model = liteModelConstructors.GetOrThrow(modelType).GetModel(id, retriever); | ||
|
||
var lite = Lite.Create<T>(id, model); | ||
retriever.ModifiablePostRetrieving((LiteImp)lite); | ||
return lite; | ||
} | ||
|
||
public override int? Count | ||
{ | ||
get { return rows.IsValueCreated ? rows.Value.Count : (int?)null; } | ||
} | ||
|
||
public override Type Type | ||
{ | ||
get { return typeof(Lite<T>); } | ||
} | ||
|
||
public override ITable Table | ||
{ | ||
get { return table; } | ||
} | ||
|
||
class ToStringColumnsFinderVisitor : ExpressionVisitor | ||
{ | ||
ParameterExpression param; | ||
|
||
HashSet<IColumn> columns; | ||
|
||
Table table; | ||
|
||
public ToStringColumnsFinderVisitor(ParameterExpression param, HashSet<IColumn> columns, Table table) | ||
{ | ||
this.param = param; | ||
this.columns = columns; | ||
this.table = table; | ||
} | ||
|
||
public static Expression GatherColumns(LambdaExpression lambda, Table table, HashSet<IColumn> columns) | ||
{ | ||
ToStringColumnsFinderVisitor toStr = new ToStringColumnsFinderVisitor( | ||
lambda.Parameters.SingleEx(), | ||
columns, | ||
table | ||
); | ||
|
||
var result = toStr.Visit(lambda.Body); | ||
|
||
return result; | ||
} | ||
|
||
|
||
|
||
static MethodInfo miMixin = ReflectionTools.GetMethodInfo((Entity e) => e.Mixin<MixinEntity>()).GetGenericMethodDefinition(); | ||
|
||
protected override Expression VisitMember(MemberExpression node) | ||
{ | ||
if (node.Expression == param) | ||
{ | ||
var field = table.GetField(node.Member); | ||
var column = GetColumn(field); | ||
columns.Add(column); | ||
return node; | ||
} | ||
|
||
if (node.Expression is MethodCallExpression me && me.Method.IsInstantiationOf(miMixin)) | ||
{ | ||
var type = me.Method.GetGenericArguments()[0]; | ||
var mixin = table.Mixins!.GetOrThrow(type); | ||
var field = mixin.GetField(node.Member); | ||
var column = GetColumn(field); | ||
columns.Add(column); | ||
return node; | ||
} | ||
|
||
return base.VisitMember(node); | ||
} | ||
|
||
protected override Expression VisitMethodCall(MethodCallExpression node) | ||
{ | ||
var obj = base.Visit(node.Object); | ||
var args = base.Visit(node.Arguments); | ||
|
||
LambdaExpression? lambda = ExpressionCleaner.GetFieldExpansion(obj?.Type, node.Method); | ||
|
||
if (lambda != null) | ||
{ | ||
var replace = ExpressionReplacer.Replace(Expression.Invoke(lambda, obj == null ? args : args.PreAnd(obj))); | ||
|
||
return this.Visit(replace); | ||
} | ||
|
||
if (node.Object == param && node.Method.Name == nameof(node.ToString)) | ||
{ | ||
columns.Add(this.table.ToStrColumn!); | ||
return node; | ||
} | ||
|
||
return base.VisitMethodCall(node); | ||
} | ||
|
||
private IColumn GetColumn(Field field) | ||
{ | ||
if (field is FieldPrimaryKey || field is FieldValue || field is FieldTicks) | ||
return (IColumn)field; | ||
|
||
throw new InvalidOperationException("{0} not supported when caching the ToString for a Lite of a transacional entity ({1})".FormatWith(field.GetType().TypeName(), this.table.Type.TypeName())); | ||
} | ||
} | ||
|
||
internal override bool Contains(PrimaryKey primaryKey) | ||
{ | ||
return this.rows.Value.ContainsKey(primaryKey); | ||
} | ||
} |
Oops, something went wrong.
160ddc3
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.
Presenting Lite Model
This commit merges some important changes that have taking almost a week from me an @JafarMirzaie working together. About 1000 lines changed and about 100 fields modified just in Framework.
All to get this:
You see this little round faces... took 1000 lines of code :)
Of course this could be done before using
formatters
andhiddenColumns
in theQuerySetting
, but the new solution is better because when you add a filter the face also goes up.and the autocomplete
Or inside of the entity
How it works in 3 simple steps.
Expresson<T>
determining the necessary fields.EntitySettings.renderLite
in the client-sideThis new extension point,
renderLite
, is currently used when rendering aLite<T>
in a nEntityLine
,EntityCombo
,EntityLink
(used inSearchControl
results),SearchValue
, etc...There is also a new
renderEntity
used for cases where theEntityLine
/EntityCombo
takes a full entity, but it's not necesary to override it in most cases since if will fall-back torenderLite
if needed.That's it! With just a few lines you can retrieve the necessary information and customize how a
Lite<T>
is rendered for some customT
everywhere in the application.Can an entity have more than Lite Model type?
Yes! The model
RegisterLiteModelConstructor
has an optional parameterisDefault
with the default value oftrue
.This is the most common case, and overrides the framework globally so that every
Lite<EmployeeEntity>
will come with aEmployeeLiteModel
inside.But when
isDefault
is set tofalse
, then all theLite<EmployeeEntity>
will continue comming with anstring
inside, and you can opt-in in a query by doing:Of, even better, can be overriden for a particular property.
If even works with
ImplementedBy
properties, allowing you to override the Lite Model for each implementation:Why it took 1000s of lines changed
Server Side (C#)
This change generalizes in
Lite<T>
the fieldstring? toStr
in toobject? model
. The model is by default anstring
but if necessary can be type inheritingModelEntity
(if needed, this restriction could be removed in the future).Internally, this means inportant changes in
Database
API, the LINQ provider andCacheLogic
.For the end-user the change is transparent in C#, since
toStr
is private anyway. Just remember that since the model replace the string, you have to overrideToString
in the model as well!!!Client Side (Typescript)
The client side also has some important internal changes, like allowing
LambdaToJavascriptConverter
to translate theLiteModalConstructor
expressions to Javascript to you can dotoLite()
in the client-side as well.Unfortunately, in the client-side the change is more disruptive for the end-user, since the field
toStr
was public.So we have built a Signum.Upgrade
Upgrade_20220623_ToStrGetToString
that replaces every occurence likea.b.c.toStr
togetToString(a.b.c)
, but you will need to importgetToString
manually in every file:Conclusion
I think this is an important step in helping Signum Framework avoid some of the tradictional limitations, comparable to
MixinEntity
,PrimaryKey
orVirtualMList
.Hopefully if will help you build more user-friendly and intuitive applications.
Enjoy!
160ddc3
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.
Very impressive, perfect
160ddc3
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.
GREAT framework improvement! 🚀 with Fantastic design 👏👏👌
Hooray! 🎉
Thanks!
160ddc3
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.
Great improvement 🥇
160ddc3
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.
Very nice, looking forward to use it!