The documentation comes from the Markdown files in the source code, so is always up-to-date but available only in English. Enjoy!
MList<T>
is a collection type that you can use to model One-To-Many and Many-To-Many relationships on Signum Framework.
This class is a full featured collection with all the methods available in List<T>
, but it supports data binding (like ObservableCollection<T>
) and has change tracking embedded.
We have already agreed that Signum Framework is all about writing the entities code and letting the engine have control over the database schema. MList<T>
is not an exception, and in some senses it is the most radical decisions about database mapping.
Usually you make a One-To-Many relationship between Country and Continent by adding a foreign key in Country pointing to Continent. This is Databases 101, and is what will happen if you add a ContinentEntity
field in CountryEntity
.
However, if you add in ContinentEntity
a field like this:
public MList<CountryEntity> Countries { get; set; } = new MList<CountryEntity>();
Then the behavior is different, a relational table with name ContinentCountries is created that looks like this:
CREATE TABLE ContinentCountries(
Id INT IDENTITY NOT NULL PRIMARY KEY,
idParent INT NOT NULL,
idCountry INT NULL
)
CREATE INDEX IX_idParent ON ContinentCountries(idParent)
CREATE INDEX IX_idCountryEntity ON ContinentCountries(idCountry)
ALTER TABLE ContinentCountries ADD CONSTRAINT FK_ContinentCountries_idParent FOREIGN KEY (idParent) REFERENCES ComputerEntity(Id)
ALTER TABLE ContinentCountries ADD CONSTRAINT FK_ContinentCountries_idCountry FOREIGN KEY (idCountry) REFERENCES CountryEntity(Id)
This table has the following columns:
Id
: This column is used internally by the Signum Framework to keep track of each particular element in the list.idParent
: Contains a reference to the entity that owns the collection, in this case the ContinentEntity.idCountry
: The actual translation of T
in the database. In this case, since CountryEntity
is an entity, a foreign key with name idCountry
is enough.This last point is very important. Tables generated by MList<T>
will be relational tables just in the case that T is another entity, but it could be almost any other thing, so maybe MList Table is a better name.
Other examples could be:
MList<strings>
in a PersonEntity
entity, the string column will be included in he MList table. The result will looks like this:CREATE TABLE PersonDNTelepones(
Id INT IDENTITY NOT NULL PRIMARY KEY,
idParent INT NOT NULL,
Value NVARCHAR(200) NULL
)
CREATE INDEX IX_idParent ON PersonDNTelepones(idParent)
ALTER TABLE PersonDNTelepones ADD CONSTRAINT FK_PersonDNTelepones_idParent FOREIGN KEY (idParent) REFERENCES PersonEntity(Id)
Is also impotant to note that, in order to reduce type-mismatch, by default the nullability of elements will be exactly the same in SQL than in C#. That means that a MList with entities of embedded entities could contain null values!. 99.9% of the time you don't want that, so avoid it using [NotNullableAttribute]
in your MList<T>
field.
Even better, always use fieldMlist
snippet to create MList fields.
In the previous version of Signum Framework, MList<T>
only had two states: Clean and Modified. When an entity with a modified MList<T>
was being saved, all their elements in the MList table where deleted and re-inserted.
Currently, MList<T>
internally remembers the RowId
of each element. This let's the engine be smarter, doing only the necessary INSERTS / DELETES / UPDATES, getting some performance improvements.
Even more important than the performance is that now the RowId are more stable, allowing scenarios like translate string fields in collections.
If you manipulate the entity with the typical methods (Add
, AddRange
, Remove
, RemoveAt
, RemoveAll
, RemoveRange
) or using the indexer, the MList will remember the RowId of all the unaffected elements, inserting the new ones when saved.
PersonEntity person = Database.Retrieve<PersonEntity>(1);
person.Telephones.RemoveAt(0); //Ok
person.Telephones.Add("664 434 423"); //Ok
person.Telepgones.RemoveAll(t=>!t.StartsWith("6")); //Ok
On the other side, if you modify a retrieved entitiy by re-assign the MList field, typically using a LINQ query and ToMList
extension method, all the previous elements will be removed and replaced by the new ones, even if they have the same values!.
PersonEntity person = Database.Retrieve<PersonEntity>(1);
person.Telephones = person.Telephones.Where(t=>t.StartWith("6")).ToMList(); //All elements will be replaced!
In order to re-set all the elements of a retrieved entity, use ResetRange
method in the MList instead. This way only the necessary changes will be made and the RowIds will be more stable.
PersonEntity person = Database.Retrieve<PersonEntity>(1);
person.Telephones.ResetRange(person.Telephones.Where(t=>t.StartWith("6"))); //Ok
Microsoft SQL Server (as many other RDBMS) does not guarantee any particular order if no ORDER BY
is included in the query. That means that when an entity with an MList<T>
field is retrieved the elements could be in a different order than they where when saved.
In many situations the order doesn't matter and this is not an issue, but if order matters, then write a [PreserveOrderAttribute]
in your MList<T>
field. This will have the following effect:
Order
column will be included in the MList table.Order
column.MList<T>
order will be sorted according to the Order
column.MList<T>
will mark the MList<T>
as modified (it doesn't normally).Additionally, when binding the MList<T>
property to any user interface control, like EntityList
, EntityStrip
, etc... Move
will be set to true, typically showing arrows to move elements up and down.
Let's consider this two design alternatives:
OrderEntity
is an Entity
with a MList<OrderLineEmbedded>
.OrderLineEmbedded
is an EmbeddedEntity
OrderEntity
is an Entity
.OrderLineEntity
is also an Entity
with a reference to OrderEntity
.From a strictly relational point of view, this scenarios are very similar, in both cases there is a 1-to-N relationship from Order
table to OrderLine
(OrderLines
in the first example).
But there are other considerations:
OrderLineEntity
every time an OrderEntity
is retrieved, so will usually be slower if there are many lines.OrderLineEntity
as part of the OrderEntity
, for example to check that the TotalPrice
of the OrderEntity
is the sum of each SubTotalPrice
of OrderLineEntity
.OrderLineEntity
by saving the OrderEntity
, even if the user interface will promote this, while in the second example you'll typically navigate to the OrderLineEntity
to make the changes, and will be logged independently.OrderEntity
to OrderLineEntity
, while in the back reference example is the other way around. This could be a problem if the two entities are in different modules.In this particular case, MList
makes more sense, but for other cases, like Countries in a Continent, a back reference and expressionMethods are better options.
Imagine the case with Order
and OrderLine
, the desired UI and validation is controlled by the Order
, so Back reference is out of qestion,
but a MList of embeddeds also has his limitations. For example:
OrderLine
has his own MList<DiscountEmbedded>
. It's not allowed to have nested MList so the only solution is to make OrderLine and Entity instead of an Embedded.OrderLineEmbedded
(like matching OrderLine with InvoiceLine). You can not have a reference to an embedded entity, so again, the OrderLine should be an Entity.The problem is that if we make an MList<OrderLineEntity>
then we have a N-to-M relationship between Orders and Lines, in other words, the database structure will allow an OrderLine to be shared between two invoices!
What we want it an MList<T>
in the UI, but a back reference in the database. This is what a VirtualMList
is.
VirtualMList require a little bit more of work:
OrderLine
) with a MList of the child entity (MList<OrderLineEntity>
). The MList should be Ignore
(avoids DB table) but QueryableProperty
(keeps querying functionality in search control). Optionally add PreserveOrder
to keep order. [Serializable, EntityKind(EntityKind.Main, EntityData.Transactional)]
public class OrderEntity : Entity
{
public DateTime OrderDate { get; set; }
(...)
[Ignore, QueryableProperty] //virtual Mlist
[NotifyChildProperty, NotifyCollectionChanged, PreserveOrder]
public MList<OrderLineEntity> Lines { get; set; } = new MList<OrderLineEntity>();
}
OrderLineEntity
) as Part
with a back reference to the main entity (OrderEntity
). If you added PreserveOrder
to keep order, you need to implement ICanBeOrdered
with an Order
property. [Serializable, EntityKind(EntityKind.Part, EntityData.Transactional)]
public class OrderLineEntity : Entity, ICanBeOrdered
{
[NotNullValidator(Disabled = true)]
public Lite<OrderEntity> Order { get; set; }
public int Order { get; set; }
public Lite<ProductEntity> Product { get; set; }
}
OrderLogic
) when registering the main entity, you need add WithMList
too. sb.Include<OrderEntity>()
.WithVirtualMList(p => p.Lines, mc => mc.Order) //<--- Register VirtualMList association
.WithQuery(() => e => new
{
Entity = e,
e.Id,
e.OrderDate,
(...)
});
That's it!. In the UI you can use a normal EntityTable
/ EntityRepeater
/ etc.. but when you remove an element in the Lines MList and save, the OrderLineEntity
will be deleted from the database.
There are some overloads of WithVirtualMList
that allow you to control how the sub-entities are saved / removed, or use your own operations to do it.
VirtualMList is more complicated but has less limitations. We recomend to use them only when this limitations are making problems and default to the more normal MList of Embedded and EntityTable
, or Back reference and SearchControl
for the common cases.
© Signum Software. All Rights Reserved.
Powered by Signum Framework