Using Entity Framework, how do you tell if Delete Cascade is enabled for a table, before removing any records from it?
public partial class DataContext : DbContext
{
public DbSet<Building> Buildings { get; set; }
public DbSet<Room> Rooms { get; set; }
}
DataContext context;
Building building;
// Will this start a DELETE CASCADE, removing Rooms within the Building?
context.Buildings.Remove(building);
This is for a generic function, so I can use DbSet<T> or DbContext but not T.
Need to do the test at runtime, just before the call to Remove().
Can you detect a DELETE CASCADE before doing the delete, and if so, how?
You could pretty easily use a t-sql function or query that gives you the information you're after. Try wrapping the following with whatever predicates you want (e.g. WHERE PK.TABLE_NAME = 'My Table' AND C.DELETE_RULE = 'CASCADE'. If a record a exists, then you've got the information you need.
SELECT
FK_TableName = FK.TABLE_SCHEMA + '.' + FK.TABLE_NAME,
FK_ColumnName = CU.COLUMN_NAME,
PK_TableName = PK.TABLE_SCHEMA + '.' + PK.TABLE_NAME,
PK_ColumnName = PT.COLUMN_NAME,
ConstraintName = C.CONSTRAINT_NAME,
DeleteRule = C.DELETE_RULE
FROM
INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS C
INNER JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS FK
ON C.CONSTRAINT_NAME = FK.CONSTRAINT_NAME
INNER JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS PK
ON C.UNIQUE_CONSTRAINT_NAME = PK.CONSTRAINT_NAME
INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE CU
ON C.CONSTRAINT_NAME = CU.CONSTRAINT_NAME
INNER JOIN (
SELECT
i1.TABLE_NAME,
i2.COLUMN_NAME
FROM
INFORMATION_SCHEMA.TABLE_CONSTRAINTS i1
INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE i2
ON i1.CONSTRAINT_NAME = i2.CONSTRAINT_NAME
WHERE
i1.CONSTRAINT_TYPE = 'PRIMARY KEY'
) PT ON PT.TABLE_NAME = PK.TABLE_NAME;
You could then wrap that into a DbSet extension and call it like context.Set<MyEntity>().UsesCascadeDelete() which fires the above query with your predicates and whatever else you want.
Because EF can run TSQL queries very easily I would still consider them a 'part' of EF.
Would 'Read FK Metadata' or 'Check an entity for FK usage' be helpful? It seems there are a couple of ways to try, though I have not tried either.
I usually check manually in the mappings files and code repositories with that knowledge in mind. Maybe the previous Stackoverflow answers will enable you to create something more generic if you need.
Related
I'm relatively new to EF, so my apologies if this is an EF 101 type of question. I have a SQL query that I'm trying to convert to C#. The following SQL query returns 290,903 records:
SELECT clcl.CLCL_ID, clcl.CLST_MCTR_REAS, clcl.CLCL_LOW_SVC_DT, clcl.CLCL_ME_AGE, grgr.CICI_ID, clcl.NWNW_ID
FROM [FACETS].[dbo].[CMC_CLCL_CLAIM] clcl WITH(NOLOCK)
INNER JOIN [FACETS].[dbo].[CMC_CLED_EDI_DATA] cled WITH(NOLOCK)ON clcl.CLCL_ID = cled.CLCL_ID
INNER JOIN [FACETS].[dbo].[CMC_PRPR_PROV] prpr WITH(NOLOCK)ON clcl.PRPR_ID = prpr.PRPR_ID
INNER JOIN [FACETS].[dbo].[CMC_SBSB_SUBSC] sbsb WITH(NOLOCK)ON clcl.SBSB_CK = sbsb.SBSB_CK
INNER JOIN [FACETS].[dbo].[CMC_SBAD_ADDR] sbad WITH(NOLOCK)ON clcl.SBSB_CK = sbad.SBSB_CK
AND sbsb.SBAD_TYPE_HOME = sbad.SBAD_TYPE
INNER JOIN FACETS.dbo.CMC_GRGR_GROUP grgr WITH(NOLOCK)ON grgr.GRGR_CK = clcl.GRGR_CK
LEFT OUTER JOIN [FACETS].[dbo].[CMC_CLHP_HOSP] clhp WITH(NOLOCK)ON clcl.CLCL_ID = clhp.CLCL_ID
LEFT OUTER JOIN [FACETS].[dbo].[CMC_HBCD_BILL_DESC] hbcd WITH(NOLOCK)ON hbcd.HBCD_ID = clhp.CLHP_FAC_TYPE + clhp.CLHP_BILL_CLASS
WHERE clcl.CLCL_CUR_STS IN ('11', '15')
I'm trying to convert it to C# with the following code:
ppoClaims = (from clcl in _context.CMC_CLCL_CLAIM
join cled in _context.CMC_CLED_EDI_DATA on clcl.CLCL_ID equals cled.CLCL_ID
join prpr in _context.CMC_PRPR_PROV on clcl.PRPR_ID equals prpr.PRPR_ID
join sbsb in _context.CMC_SBSB_SUBSC on clcl.SBSB_CK equals sbsb.SBSB_CK
join sbad in _context.CMC_SBAD_ADDR on clcl.SBSB_CK equals sbad.SBSB_CK
join grgr in _context.CMC_GRGR_GROUP on clcl.GRGR_CK equals grgr.GRGR_CK
join clhp in _context.CMC_CLHP_HOSP on clcl.CLCL_ID equals clhp.CLCL_ID into SUBclhp
from z in SUBclhp.DefaultIfEmpty()
join hbcd in _context.CMC_HBCD_BILL_DESC on z.CLHP_FAC_TYPE + z.CLHP_BILL_CLASS equals hbcd.HBCD_ID
where sbsb.SBAD_TYPE_HOME == sbad.SBAD_TYPE
&& staticVars.CLCL_CUR_STS.Contains(clcl.CLCL_CUR_STS)
However it only return 48,930 records, so clearly I'm doing something wrong.
I think my problem could lie in one of two spots. Either the
AND sbsb.SBAD_TYPE_HOME = sbad.SBAD_TYPE
being put in my where clause in the C#. Or my attempt at the left outer joins here
join clhp in _context.CMC_CLHP_HOSP on clcl.CLCL_ID equals clhp.CLCL_ID into SUBclhp
from z in SUBclhp.DefaultIfEmpty()
join hbcd in _context.CMC_HBCD_BILL_DESC on z.CLHP_FAC_TYPE + z.CLHP_BILL_CLASS equals hbcd.HBCD_ID
Or, maybe it's a combination of both? I feel like I'm pretty close, just need one or two small alterations. Any help is much appreciated.
Edit: The staticVars.CLCL_CUR_STS in the C# is an array that contains "11" and "15"
Firstly, EF is an ORM not merely a replacement for ADO. You map entities to tables, but more importantly, you map the relations between these entities so that EF can work out the joins for you automatically. The fact that EF's Linq queries have a join operation is merely for exceptional scenarios. The goal with EF isn't to just give you a different way to write SQL queries, it is to avoid writing SQL-like queries all-together.
To that end, each entity that contains relations to other entities should contain references and collections for the various many-to-one or one-to-many relationships, or many-to-many or one-to-one relationships respectively.
Your job with EF starts with mapping out these relationships and creating the appropriate navigation properties. This also gives you the opportunity to give your entities a more friendly, readable name where the database table conforms to legacy naming conventions/requirements. For example, looking briefly at your schema:
[Table("CMC_CLCL_CLAIM")]
public class Claim
{
[Key, Column("CLCL_ID")]
public int Id { get; set; }
[Column("CLCL_CUR_STS")]
public string CurrentStatus { get; set; }
// ... other columns ...
public virtual ICollection<Group> Groups { get; set; }
// ... and other references, collections, etc.
}
[Table("CMC_GRGR_Group")]
public class Group
{
[Key, Column("GRGR_ID")]
public int Id { get; set; }
//... Other group fields...
[ForeignKey("Claim"), Column("CLCL_ID")]
public int ClaimId { get; set; }
public virtual Claim Claim { get; set; }
// Probaby a FK to another table?
public int CiCiId { get; set; }
}
EF can automatically work out many relationships, but there can be cases where it helps to do it a bit more explicitly with attributes and/or EntityTypeConfiguration / DbContext.OnModelCreating which can help streamline entities to avoid bi-directional references if not needed, or declaring FK properties. (That's a bit more advanced material to research, but for a start focus on setting up the relations in the first place)
Once these navigation properties are set up, then you build your Linq expressions around the entities and let EF build your SQL. From that you can compare the results and determine if all of the relationships are working properly.
For instance you can load the entity and its related data, or project the data from this entity graph (the entity and related entities) into a desired view model or anonymous type.
So for instance in your example query, it doesn't really make sense to be joining all of those tables when the SELECT is only pulling data from the CLCL Claim and one column from the GRGR Group which joins off the Claim, none of the other tables.
The query would look more like:
var data = _context.Claims
.Where(x => staticVars.ActiveStatuses.Contains(x.CurrentStatus))
.SelectMany(x => x.Groups.Select( g => new
{
x.Reason,
x.LowServiceDate,
x.MedianAge,
g.CiCiId,
x.NwnwId
}).ToList();
Based on the joins you want a list of these GroupIds with the relevant details from the claim where one claim can have multiple Groups. Again the entity property names are guesses at whatever the original schema fields mean. It's more for demonstration that you can use more meaningful names in your entities to make your code easier to understand rather than propagating archaic naming conventions.
Ultimately the Joins in the original tables can possibly be resulting in further Cartesian products. If the goal in the original query with the unused INNER JOINS was just to enforce limiting data for Null-able FKs to return data where those FKs are populated (rather than using non-nullable FKs) then the Where clause above can be updated to AND (&&) in conditions where the associated related entities are != null.
In any case, when working with EF, it is better to set up the relationships between the entities, then approach the requirements as "I want to return this data from the entity graph in this format with these filters etc." rather than "I want to reproduce this SQL query".
If instead you just want to port SQL into C# then I'd recommend just writing Stored Procedures or Views and mapping read-only entities to those outputs rather than trying to map tables and trying to build Linq versions of those queries.
That title is not very good, so consider the following. I have five tables:
User {
Id,
ProfileId // -> Profiles.Id
}
Profile {
Id
}
ProfilePermissionSets {
ProfileId // -> Profiles.Id
PermissionSetId // -> PermissionSets.Id
}
UserPermissionSets {
UserId // -> Users.Id
PermissionSetId // -> PermissionSets.Id
}
PermissionSets {
Id
}
Permissions {
Id,
PermissionSetId // -> PermissionSets.Id
}
And I want get all of the permissions for a user that are directly linked to it or indirectly through the profile. The not-quite-there SQL I've come up with so far is this:
SELECT [Pe].[Controller],
[Pe].[Action]
FROM [PermissionSets] AS [PS]
JOIN [UserPermissionSets] AS [UPS]
ON ([UPS].[PermissionSetId] = [PS].[Id])
JOIN [Users] AS [U]
ON ([U].[Id] = [UPS].[UserId])
JOIN [Profiles] AS [P]
ON ([P].[Id] = [U].[ProfileId])
JOIN [ProfilePermissionSets] AS [PPS]
ON ([PPS].[ProfileId] = [P].[Id])
JOIN [Permissions] AS [Pe]
ON ([Pe].[PermissionSetId] = [PS].[Id])
WHERE [U].[Id] = 4;
It returns back a correct count of rows, but it's repeating the controller or action over and over, so it's wrong. I'm hoping someone can help me correct it to show all of the distinct permission sets for the user. Ideally, I'd like to also change it so that it's all discovered starting at the user because that is what I have access to in the method I need to do this (the object is an Entity Framework class named User and will be browsed using LINQ).
UPDATED because I forgot that I really wanted the permissions not the permission sets.
Try this SQL
SELECT [Pe].[Controller],
[Pe].[Action]
FROM [Users] AS [U]
LEFT OUTER JOIN [UserPermissionSets] AS [UPS]
ON ([UPS].[UserId] = [U].[Id])
LEFT OUTER JOIN [ProfilePermissionSets] AS [PPS]
ON ([PPS].[ProfileId] = [U].[ProfileId])
LEFT OUTER JOIN [Permissions] AS [Pe]
ON ([Pe].[PermissionSetId] = [UPS].[PermissionSetId])
OR ([Pe].[PermissionSetId] = [PPS].[PermissionSetId])
WHERE [U].[Id] = 4;
So, messing around on LINQPad, I came up with this as far as the LINQ query:
user.PermissionSets.Union(user.Profile.PermissionSets).SelectMany(
ps =>
ps.Permissions.Select(
p =>
p.Controller + "." + p.Action));
And it produces what I want, BUT it does it by composing the results of a bunch of SQL queries. The biggest impact comes from profiles that have multiple permission sets, like say the Administrator. I don't think there's a way around it, and I only have a User object to work with, so I'm ok with the excess SQL queries, at least for now.
I have the following tables in my database:
SageAccount
ID (bigint)
LegacyID (nvarchar)
Customer (bit)
Consignments
ID (bigint)
Customer (nvarchar)
What I want to do is have a navigation property/association in my Linq to Sql dbml from Consignment to SageAccount. The difficulty with this is that not only do we need to match SageAccount.LegacyID => Consignments.Customer but we also need to only join to sage accounts where SageAccount.Customer is TRUE. So on the Consignments end, it isn't joining onto a field but instead a static value.
Is this possible in Linq to Sql? Note this database doesn't (and unfortunately can't) have any foreign keys setup in the database.
Yes it is possible. linq have join method. You can use it ike this in your situation:
var res = from sageAccount in _context.SageAccount
join consignments in _context.Consignments
on
new
{
LegacyID = sageAccount.LegacyID,
Customer = sageAccount.Customer
}
equals
new
{
LegacyID = consignments.ID,
Customer = true
}
select new { SageAccountID = sageAccount.ID };
Note that Property name, Type and order in the anonymous objects that you're joining on must match.
You can't use OR and AND in joins - use just equals one object to other.
This will have a this kind of result in your SQL:
SELECT [t0].[ID] AS [SageAccountID]
FROM [dbo].[SageAccount] AS [t0]
INNER JOIN [dbo].[Consignments] AS [t1] ON (([t0].[LegacyID]) = [t1].[ID])
AND ([t0].[Customer] = 1)
Try to do a single database call to get an entity, as well as the count of related child entities.
I know I can retrieve the count using
var count = Context.MyChildEntitySet.Where(....).Count();
or even MyEntity.ListNavigationProperty.Count()
But That means getting the entity first, followed by another call in order to get the count or use an Include which would retrieve the whole list of related entities.
I am wondering is it possible to add a "Computed" column in SQL Server to return the Count of related rows in another table?
If not how do I ask EF to retrieve the related count for each entity in once call?
I am thinking of possibly using Join with GroupBy, but this seems an Ugly solution/hack.
public class MyEntity
{
public uint NumberOfVotes{ get; private set; }
}
which ideally woudl generate SQL Similar to:
SELECT
*,
(SELECT Count(*) FROM ChildTable c WHERE c.ParentId = p.Id) NumberOfVotes
FROM ParentTable p
UPDATED
You can always drop down to using actual SQL in the following way...
string query = "SELECT *,
(SELECT Count(*) FROM ChildTable c
WHERE c.ParentId = p.Id) as NumberOfVotes
FROM ParentTable p";
RowShape[] rows = ctx.Database.SqlQuery<RowShape>(query, new object[] { }).ToArray();
I realize this is not ideal because then you are taking a dependency on the SQL dialect of your target database. If you moved form SQL Server to something else then you would need to check and maybe modify your string.
The RowShape class is defined with the properties that match the ParentTable along with an extra property called NumberOfVotes that has the calculated count.
Still, a possible workaround.
All,
I have an entity called Client that has an association to an entity called Region as:
Client -> Region
All my entities are Lazy loaded (nhibernate default setting).
Region in Client is mapped as NotNull = false:
[ManyToOne(Column = "region_id",
ClassType = typeof(Region),
NotNull = false)]
public virtual Region Region
{
get { return _region; }
set { _region = value; }
}
When I create client criteria and set the FetchMode(FetchMode.Join), the generated select is an inner join, but I expected and left outer join since Region can be NULL.
The above happens DEPENDS on how the criteria is created. If I create the criteria as in Ex 1, I get correct SQL generated and Region is left outer joined, if I create the criteria as in Ex 2, I get the incorrect SQL generated, the Region is inner joined.
Ex 1) Correct SQL
ICriteria c = s.Session.CreateCriteria<Client>();
c.SetFetchMode("Region", NHibernate.FetchMode.Join);
IList<Client> list2 = c.List<Client>();
SELECT * FROM Companies this_ left outer join Code_Region_Types region2_ on this_.region_id=region2_.entity_id
Ex 2) Incorrect SQL
ICriteria c = s.Session.CreateCriteria<Client>();
ICriteria subC = c.CreateCriteria("Region");
c.SetFetchMode("Region", NHibernate.FetchMode.Join);
IList<Client> list2 = c.List<Client>();
SELECT * FROM Companies this_ inner join Code_Region_Types region1_ on this_.region_id=region1_.entity_id
In ex 2), the line which creates a sub-criteria
ICriteria subC = c.CreateCriteria("Region");
messes up the join clause.
This produces incorrect result since some clients may have no Region and therefore are not included in the query.
It appears the only fix for this is to specify explicitly the join on the sub-criteria:
ICriteria subC = c.CreateCriteria("Region", JoinType.LeftOuterJoin)
The above fixes the issue. Is this what Nhibernate expects?
What you are experiencing is absolutely correct. And your solution is really the proper one.
The call to:
criteria.CreateCriteria(associationPath);
in fact does internally use INNER JOIN (see here):
public ICriteria CreateCriteria(string associationPath)
{
return CreateCriteria(associationPath, JoinType.InnerJoin);
}
So, this way the Query is defined. It will be INNER JOIN. The Fetch mode, is then driven by result of that Criteria and its SubCriteria - i.e. only found results are taken into account.
But as you've found out, we can simply change that by explicit call:
ICriteria subCriteria = criteria
.CreateCriteria(associationPath, JoinType.LeftOuterJoin)
And that will do what expected...