I am using SQLite-Net PCL together with SQLite-Net extensions for the development of an application using Xamarin.
I have a one to many relationship between two classes A and B defined as follows:
public class A
{
[PrimaryKey, AutoIncrement]
public int Id
{
get;
set;
}
public string Name
{
get;
set;
}
[OneToMany(CascadeOperations = CascadeOperation.All)]
public List<B> Sons
{
get;
set;
}
public A()
{
}
public A(string name, List<B> sons)
{
Name = name;
Sons = sons;
}
}
public class B
{
[PrimaryKey, AutoIncrement]
public int Id
{
get;
set;
}
public string Name
{
get;
set;
}
[ForeignKey(typeof(A))]
public int FatherId
{
get;
set;
}
[ManyToOne]
public A Father
{
get;
set;
}
public B()
{
}
public B(string name)
{
Name = name;
}
}
What I would like to do, is to retrieve an object of type A from the database, remove one of the Sons objects of type B and update the database accordingly. This is what I have tried:
var sons = new List<B>
{
new B("uno"),
new B("due"),
new B("tre"),
};
one = new A("padre", sons);
using (var conn = DatabaseStore.GetConnection())
{
conn.DeleteAll<A>();
conn.DeleteAll<B>();
conn.InsertWithChildren(one, true);
A retrieved = conn.GetWithChildren<A>(one.Id);
retrieved.Sons.RemoveAt(1);
}
using (var conn = DatabaseStore.GetConnection())
{
var retrieved = conn.GetWithChildren<A>(one.Id);
retrieved.Sons.RemoveAt(1); //"due"
//conn.UpdateWithChildren(retrieved);
conn.InsertOrReplaceWithChildren(retrieved, true);
}
The problem is that both with UpdateWithChildren and with InsertOrReplaceWithChildren the the object is not really removed from the database, but only it's foreign key nulled. Is it possible to make it delete the son object?
You're not really trying to delete any object at all. You're just removing the relationship between the two objects, but nothing stops you from having more objects related to any of them, so deleting any is not correct as you may break other relationships.
It should be more like this:
using (var conn = DatabaseStore.GetConnection())
{
var retrieved = conn.GetWithChildren<A>(one.Id);
var due = retrieved.Sons[1];
// This is not required if the foreign key is in the other end,
// but it would be the usual way for any other scenario
// retrieved.Sons.Remove(due);
// conn.UpdateWithChildren(retrieved);
// Then delete the object if it's no longer required to exist in the database
conn.delete(due);
}
Related
I'm trying to efficiently map entities on to models.
My entities are:
public class ParentEntity
{
public int Id { get; set; }
public string Name { get; set; }
public ChildEntity Child { get; set; }
}
public class ChildEntity
{
public int Id { get; set; }
public string Name { get; set; }
}
and my models are:
public class ParentModel
{
public int Id { get; set; }
public string Name { get; set; }
public ChildModel Child { get; set; }
}
public class ChildModel
{
public int Id { get; set; }
public string Name { get; set; }
}
(In practice, there would be differences between these classes, but not here for simplification.)
I've written an extension method to do the mapping:
public static IQueryable<ParentModel> ToParentModel (this IQueryable<ParentEntity> parentEntities)
{
return parentEntities.Select(p => new ParentModel
{
Id = p.Id,
Name = p.Name,
Child = new ChildModel { Id = p.Child.Id, Name = p.Child.Name.ToLower()}
});
}
The ToLower() is there to highlight the problem.
I can run this with:
var parents = _context.Set<ParentEntity>().ToParentModel().ToArray();
The generated SQL is:
SELECT "p"."Id", "p"."Name", "c"."Id", lower("c"."Name") AS "Name"
FROM "Parents" AS "p"
LEFT JOIN "Children" AS "c" ON "p"."ChildId" = "c"."Id"
i.e. the lowercase processing is done in the database.
All good so far, except that the separation of concerns is not good. The code to initialize a ChildModel is in the same place as the code to initialize a ParentModel.
I try using a constructor in ChildModel:
public ChildModel(ChildEntity ent)
{
Id = ent.Id;
Name = ent.Name.ToLower();
}
and in the extension method:
return parentEntities.Select(p => new ParentModel
{
Id = p.Id,
Name = p.Name,
Child = new ChildModel (p.Child)
});
This works, but the generated SQL does not do contains a lower. The conversion to lowercase is done in the program.
Is there a way I can have by cake and eat it?
Can I still have my C# code converted to SQL, but still structure my C# code in a modular way?
I've one questions to my program. I'm writing a console application in C# with using entity framework. This program has two table in one-to-many relationship. The first table is called "Car" and the second is "DescriptionCar". I assume that I've data in the database and I want to add a description to a specific car using its ID.
At the moment I have something like that:
using (var context = new ContextClass())
{
Car car = context.Cars.Single(a => a.CarId == id);
DescriptionCar descriptionCar = new DescriptionCar();
}
At the beginning, I download the data of a specific car, create a new object to add a description and now I do not know how to fill in the data.
Model classes:
public class Car
{
public int CarId { get; set; }
public string Brand { get; set; }
public string Model { get; set; }
public string Fuel { get; set; }
public int ProductionYear { get; set; }
public string Color { get; set; }
public int PowerEngine { get; set; }
public virtual ICollection<DescriptionCar> DescriptionCars { get; set; }
}
public class DescriptionCar
{
public int DescriptionCarId { get; set; }
public string Description { get; set; }
public Car Car { get; set; }
}
Thanks for the help.
There are a couple different ways you can accomplish this. The easiest would be the following:
using (var context = new ContextClass())
{
DescriptionCar descriptionCar = context.DescriptionCars.Single(a => a.CarId == id);
descriptionCar.Description = "Your description";
context.SaveChanges();
}
I can assist you further if you share your entity classes.
I'm not sure if you're hung up on the EF portion of this or working with an ICollection. This should work:
//Your existing code
Car car = context.Cars.Single(a => a.CarId == id);
DescriptionCar descriptionCar = new DescriptionCar();
//Code to save a new description to your ICollection
List<DescriptionCar> descriptions = new List<DescriptionCar>(); //List to work with
if (car.DescriptionCars!= null)
{
descriptions = (List<DescriptionCar>)car.DescriptionCars; //Load existing ICollection into our List
}
descriptions.Add(descriptionCar); //Add the new description
car.DescriptionCars = descriptions; //Store the working list into the EF object
context.Save(); //Save to the database
I have three tables zRequest, zFacility and a table for a many to many relationship between the two with just the id's of each. My aim is to add a request with many facilities. But whenever I add a new request with some facilities, it adds the selected facilities to zFacility as well as adding to the relationship table.
This is the relevant code in the controller:
foreach (var facility in Facilities)
{
var facName = facility.Replace(".", " ");
var facQry = from fac in db.zFacilities where fac.FacilityName == facName select fac;
var facList = facQry.ToList();
var item = new zFacility();
item.FacilityId = facList.FirstOrDefault().FacilityId;
item.FacilityName = facList.FirstOrDefault().FacilityName;
//db.zFacility.Attach(item);
zrequest.zFacility.Add(item);
}
zrequestRepository.InsertOrUpdate(zrequest);
zrequestRepository.Save();
I have done some research and tried attaching each facility to the database via the commented line but this gave me another error because another entity of the same type already has the same primary key value
This is the code from zRequestRepository:
public void InsertOrUpdate(zRequest zrequest)
{
if (zrequest.RequestId == default(int)) {
// New entity
context.zRequests.Add(zrequest);
} else {
// Existing entity
context.Entry(zrequest).State = System.Data.Entity.EntityState.Modified;
}
}
What can I do to fix this? Let me know if I need to provide more information...
Edit, providing the relevant models as requested.
zFacility:
using System;
using System.Collections.Generic;
public partial class zFacility
{
public zFacility()
{
this.zRequest = new HashSet<zRequest>();
this.zRoom = new HashSet<zRoom>();
}
public short FacilityId { get; set; }
public string FacilityName { get; set; }
public virtual ICollection<zRequest> zRequest { get; set; }
public virtual ICollection<zRoom> zRoom { get; set; }
}
zRequest:
using System;
using System.Collections.Generic;
public partial class zRequest
{
public zRequest()
{
this.zFacility = new HashSet<zFacility>();
this.zRoom = new HashSet<zRoom>();
}
public int RequestId { get; set; }
public string ModCode { get; set; }
public short StatusId { get; set; }
public int WeekId { get; set; }
public short DayId { get; set; }
public short PeriodId { get; set; }
public short SessionLength { get; set; }
public short Semester { get; set; }
public short RoundNo { get; set; }
public string SpecialRequirement { get; set; }
public short UserId { get; set; }
public virtual zDay zDay { get; set; }
public virtual zPeriod zPeriod { get; set; }
public virtual zRound zRound { get; set; }
public virtual zStatus zStatus { get; set; }
public virtual zWeek zWeek { get; set; }
public virtual ICollection<zFacility> zFacility { get; set; }
public virtual ICollection<zRoom> zRoom { get; set; }
public virtual zUser zUser { get; set; }
}
These were both generated via a database-first.
EF by default assume that the selected facilities (zFacility) are new and insert them into the database.
To avoid that , you will need to change the state to Unchanged for zFacility
Something like this
public void InsertOrUpdate(zRequest zrequest)
{
if (zrequest.RequestId == default(int)) {
context.zFacility.Attach(zrequest); // state Unchanged for zFacility
// New entity
context.zRequests.Add(zrequest);
} else {
// Existing entity
context.Entry(zrequest).State = System.Data.Entity.EntityState.Modified;
}
}
What you'll need to do is to select zFactories objects from DB instead of creating new object for each one of them because that makes EF think that you want to create completely new record.
So:
Select zFactories
Add them to the zRequest.zFacility (if they're not already there)
Save
It's a little hard to answer this question as there's some holes in your code. Based on usage, I'm assuming that Facilities is an enumerable of strings, and is created from the selected items in your post data.
I'm not sure why you're creating a new zFacility instead of using the one that you've already pulled from the database. Doing it the way you are, you're also introducing the possibility of null reference exceptions if there's no matching facility.
Anyways, I would change the code you have to:
// This is so you can run a single minimal query to select
// all the facilities you'll be working with
var facNames = Facilities.Select(m => m.Replace(".", " "));
var facList = db.zFacilities.Where(m => facNames.Contains(m.FacilityName)).ToList();
// Now add all facilities not currently attached
facList.Where(m => !zrequest.zFacility.Contains(m)).ToList()
.ForEach(m => zrequest.zFacility.Add(m));
You probably will also need to remove items that have been deselected. The easiest way to do that is to do the last bit of code in reverse:
zrequest.zFacility.Where(m => !facList.Contains(m)).ToList()
.ForEach(m => zrequest.zFacility.Remove(m));
EDIT
Actually, a more succinct way to remove deselected items would be:
zrequest.zFacility.RemoveAll(m => !factList.Contains(m));
I am having troubles trying to figure out how to use the EF6 interceptors to set a value on Insert/Update.
What I wanted to do is to have an interceptor to automatically create a new instance of Audit like so:
public class FooContext : DbContext
{
public DbSet<Invoice> Invoices { get; set; }
public DbSet<Audit> Audits { get; set; }
}
public class Invoice
{
public int Id { get; set; }
public string Name { get; set; }
public Audit AuditAndConcurrencyKey { get; set; }
}
public class InvoiceItem
{
public int Id { get; set; }
public Invoice Header { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
//For legacy reasons. I know this design is wrong :(
public Audit AuditAndConcurrencyKey { get; set; }
}
public class Audit
{
public int Id { get; set; }
public int InstanceId { get; set; }
public string Message { get; set; }
}
[Test]
public void WillCreateAudit()
{
using (var db = new FooContext())
{
var inv = new Invoice {Name = "Foo Invoice"};
var invLine = new InvoiceItem {Header = inv, Price = 1, Name = "Apple"};
db.Invoices.Add(inv);
db.SaveChanges();
//Inceptors should figure out that we are working with "Invoice" and "InvoiceLine"
//And automatically create an "Audit" instance
Assert.That(inv.AuditAndConcurrencyKey != null);
Assert.That(invLine.AuditAndConcurrencyKey != null);
Assert.That(inv.AuditAndConcurrencyKey == invLine.AuditAndConcurrencyKey)
}
}
The first thing I checked is this example for SoftDeleteInterceptor. I don't think this is what I want because it looks like at the point where we are already generating the expression tree, we are no longer aware of the type of object you are working with.
I checked this example as well, but again, it looks like we are injecting strings instead of setting object references.
Ideally I want something like this:
public class AuditInterceptor
{
public void Intercept(object obj)
{
if (!(obj is Invoice) && !(obj is InvoiceItem))
return; //not type we are looking for, by-pass
//Set the audit here
}
}
I have two classes..
class Assembly
{
public Assembly()
{
this.Tasks = new List<Task>();
}
public int AssemblyId { get; set; }
public string PartNum { get; set; }
public string Desc { get; set; }
public List<Task> Tasks { get; set; }
}
and
class Task
{
public int TaskId { get; set; }
public string Name { get; set; }
public string Desc { get; set; }
public int workMins { get; set; }
public List<Assembly> Assemblies { get; set; }
}
These classes are set up with a many-to-many relationship.
I have a list of tasks that have already been saved to the database and I am trying to add one of these tasks to a new assembly.
The issue I am haing is when I try to create the new assembly with tasks and save it to the database.
using (CapacityContext ctx = new CapacityContext())
{
Assembly a = new Assembly();
a.PartNum = cmbPartNum.Text;
a.Desc = txtDesc.Text;
foreach (DataGridViewRow dr in gridTasks.Rows)
{
int taskId = Convert.ToInt16(dr.Cells[colTaskId.Name].Value);
Task at = Task.GetTaskById(taskId);
a.Tasks.Add(at);
}
ctx.Assemblies.Add(a);
ctx.SaveChanges();
}
Once the changes are sent to the database, a new Task is inserted to the Tasks table that is identical except that it has a new TaskId. I checked the entitiy before it is saved and the correct task with the correct taskId is added, so I'm at a loss about what I'm doing wrong.
Edit:
For anyone else who runs into this, I found the problem. I had created this method
public static Task GetTaskById(int id)
{
using (CapacityContext ctx = new CapacityContext())
{
try
{
return ctx.Tasks.Find(id);
}
catch (InvalidOperationException)
{
return null;
}
}
}
which retrieves a taks by its id. This was the cause of the problem as it's a no-no to insert an entitiy using a Find() from a different context. Once I moved ctx.Tasks.Find(id); inside the same method that creates the assembly, it worked fine!