I am trying to use SQLite-Net Extensions to create a Relational Database. I'm running into an issue when trying to pull the Term object from the database. It successfully pulls over its associated courses, but not the courses associated assessments and notes. I'm not sure if the problem lies in how I insert the objects into the database, how I pull the objects from the database, or how I have the objects attributes listed.
I feel like the SQLite-Net Extensions documentation is extremely limited, so I'm not even sure what's going on. I've tried it many different ways, including adding CascadeOperations, but non of those seemed to help.
Here is the (simplified) code for my objects:
[Table("Terms")]
public class Term
{
[PrimaryKey, AutoIncrement]
public int ID { get; set; }
public string Name { get; set; }
[OneToMany]
public List<Course> Courses { get; set; }
public Term() { }
public Term(string name, List<Course> courses)
{
Name = name;
Courses = courses;
}
Courses
[Table("Courses")]
public class Course
{
[PrimaryKey, AutoIncrement]
public int ID { get; set; }
[ForeignKey(typeof(Term))]
public int TermID { get; set; }
public string Name { get; set; }
[OneToMany]
public List<Assessment> Assessments { get; set; }
[OneToMany]
public List<Note> Notes { get; set; }
public Course() { }
public Course(string name, List<Assessment> assessments, List<Note> notes)
{
Name = name;
Assessments = assessments;
Notes = notes;
}
}
Assessments
[Table("Assessments")]
public class Assessment
{
[PrimaryKey, AutoIncrement]
public int ID { get; set; }
[ForeignKey(typeof(Course))]
public int CourseID { get; set; }
public string Name { get; set; }
public Assessment() { }
public Assessment(string name)
{
Name = name;
}
}
Notes
[Table("Notes")]
public class Note
{
[PrimaryKey, AutoIncrement]
public int ID { get; set; }
[ForeignKey(typeof(Course))]
public int CourseID { get; set; }
public string Name { get; set; }
public string Text { get; set; }
public Note() { }
public Note(string name, string note)
{
Name = name;
Text = note;
}
}
And here is the code for inserting and getting objects:
Inserting
public bool SaveTermAsync(Term term)
{
if (term.ID != 0)
{
_database.UpdateWithChildrenAsync(term);
return true;
}
else
{
foreach (var course in term.Courses)
{
foreach (var assessment in course.Assessments)
{
_database.InsertAsync(assessment);
}
foreach (var note in course.Notes)
{
_database.InsertAsync(note);
}
_database.InsertAsync(course);
}
_database.InsertAsync(term);
_database.UpdateWithChildrenAsync(term);
return false;
}
}
Getting
public Task<List<Term>> GetTermsAsync()
{
return _database.GetAllWithChildrenAsync<Term>();
}
I know it's a bit of a code dump, but I have no idea where or what could be going wrong. If anyone could give any information about what is potentially going wrong, that would be awesome. Perhaps I'm simply expecting something to happen that isn't actually how it works. I don't know.
Also, if anyone has any links to some better documentation than https://bitbucket.org/twincoders/sqlite-net-extensions/src/master/ that would be awesome
EDIT
I tried using Cascading Options as well, CascadeRead, CascadeInsert, and CascadeAll. Using CascadeInsert or CascadeAll with _database.InsertWithChildrenAsync(term, true) resulted in a crash. The crash does not provide any error messages, and even wrapping the InsertWithChildren with a try catch block didn't work. Removing the recursive bool caused the program not to crash, and actually get the closest to what I'm looking for. Assessments and Notes are no longer null, but are still empty. Here's my updated code:
Saving and Getting:
public async Task<List<Term>> GetTermsAsync()
{
return await _database.GetAllWithChildrenAsync<Term>(recursive: true);
}
public async void SaveTermAsync(Term term)
{
if (term.ID != 0)
{
await _database.UpdateWithChildrenAsync(term);
}
else
{
//Trying this with recursion results in crash
await _database.InsertWithChildrenAsync(term);
}
}
One-To-Many Relationships:
//In Term
[OneToMany(CascadeOperations = CascadeOperation.All)]
public List<Course> Courses { get; set; }
//In Courses
[OneToMany(CascadeOperations = CascadeOperation.All)]
public List<Assessment> Assessments { get; set; }
[OneToMany(CascadeOperations = CascadeOperation.All)]
public List<Note> Notes { get; set; }
Also, I forgot to include last time how I'm populating the tables in the first place.
public bool CreateTables()
{
_database.CreateTableAsync<Term>().Wait();
_database.CreateTableAsync<Course>().Wait();
_database.CreateTableAsync<Assessment>().Wait();
_database.CreateTableAsync<Note>().Wait();
return true;
}
public Task<int> ClearTablesTest()
{
_database.DropTableAsync<Term>();
_database.DropTableAsync<Course>();
_database.DropTableAsync<Assessment>();
return _database.DropTableAsync<Note>();
}
async public Task<int> PopulateTestData()
{
await ClearTablesTest();
CreateTables();
Term term = new Term("Test Term", true, DateTime.Now, DateTime.Now.AddDays(10),
new List<Course>
{
new Course("Test Course", CourseStatus.Completed, "Guys Name", "(999)-999-9999", "email#gmail.com", 6, DateTime.Now, DateTime.Now.AddDays(10),
new List<Assessment>
{
new Assessment("Test Assessment", AssessmentType.Objective, false, DateTime.Now, DateTime.Now.AddDays(10))
},
new List<Note>
{
new Note("Test Note", "This is a test note.")
})
});
App.Database.SaveTermAsync(term);
return 0;
}
I finally figured out what was causing the crash as well as causing general confusion within SQLite-Net Extensions.
In my Assessment class, the property
public string BackgroundColor
{
get { return IsComplete ? "#558f45" : "Gray"; }
set { BackgroundColor = value; }
}
was causing the crash when recursion was used. I've been scouring the web for over two weeks looking for solutions to this issue, but haven't found anything similar to this. I submitted a bug report on the SQLite-Net Extensions bitbucket.
If anyone knows why this specific line would cause issues, I'd love to hear your input. Until then I'm going to mark this question as answered and continue work on my app.
Thanks #redent84 for your help thus far on this issue.
Related
I have the following method in my controller:
[HttpPost]
public async Task<ActionResult<ChartsCategoryDto>> PostChartsCategory(ChartsCategoryDto chartsCategory)
{
try
{
var category = _mapper.Map<ChartsCategoryDto, ChartsCategory>(chartsCategory);
if (category.Id == 0)
{
category = _context.ChartsCategories.Add(category).Entity;
await _context.SaveChangesAsync();
return CreatedAtAction("GetChartsCategory", new { id = category.Id }, _mapper.Map<ChartsCategory, ChartsCategoryDto>(category));
}
else
{
_context.ChartsCategories.Update(category);
await _context.SaveChangesAsync();
return NoContent();
}
}
catch (Exception)
{
return StatusCode(StatusCodes.Status500InternalServerError, "Error updating data");
}
}
Which is supposed to handle both create and update of the entity named ChartsCategory, which has a collection of Charts:
public partial class ChartsCategory
{
public ChartsCategory()
{
Charts = new HashSet<Chart>();
InverseParent = new HashSet<ChartsCategory>();
}
public int Id { get; set; }
public string Name { get; set; }
public string Label { get; set; }
public string Icon { get; set; }
public int? ParentId { get; set; }
public virtual ChartsCategory Parent { get; set; }
public virtual ICollection<Chart> Charts { get; set; }
public virtual ICollection<ChartsCategory> InverseParent { get; set; }
}
When I create a new entity - everything works as expected.
When I update an existing entity - everything works as expected.
Updating or creating a singular Chart within a ChartCategory - as expected.
Deleting a Chart within the ChartsCategory doesn't work.
I would expect the Update() functionality to remove missing items.
I found this answer however it seems a little too explicit. I want to globally state that a descendant item must be sent, otherwise it should be removed.
Thanks
I think that in the first case the retrieved user is not tracked by the context.
_context.ChartsCategories.AsNoTrcking.Remove(category);
currently I am working with the NuGet SQLite-Net-Extensions in Xamarin Forms and I have encountered a problem for which I can't find a solution.
The Problem: When calling GetWithChildren(primaryKey, recursive: true), the returned object only contains the first child layer. An example can be seen in the following.
The database is built up like this:
The equivalent code to this model is provided in the following:
User
namespace DatabaseTest
{
[Table("Users")]
public class User
{
[PrimaryKey,AutoIncrement]
public int Id { get; set; }
public string Name { get; set; }
[OneToMany]
public List<Contact> Contacts { get; set; }
}
}
Contact
namespace DatabaseTest
{
[Table("Contacts")]
public class Contact
{
[PrimaryKey,AutoIncrement]
public int Id { get; set; }
public string Name { get; set; }
[OneToMany]
public List<Entry> Entries { get; set; }
[ForeignKey(typeof(User))]
public int UserID { get; set; }
[ManyToOne]
public User User { get; set; }
}
}
Entry
namespace DatabaseTest
{
[Table("Entries")]
public class Entry
{
[PrimaryKey,AutoIncrement]
public int Id { get; set; }
public float Amount { get; set; }
[ForeignKey(typeof(Contact))]
public int ContactId { get; set; }
[ManyToOne]
public Contact Contact { get; set; }
}
}
In my App.cs I am creating the database and use the CreateTable() Method for all three classes. For the sake of this example, in MainPage.xaml.cs there is simply a button, which has a ButtonClicked Method.
In the real Application a process could look like this:
User logs in --> Adds Contact --> At some Point User creates Entry to one of his contacts
To accomplish this procedure in my example, the ButtonClicked Method looks like this:
void ButtonClicked(object sender, EventArgs e)
{
try
{
User user = new User()
{
Name = "Test user"
};
Contact contact = new Contact()
{
Name = "First contact"
};
Entry entry1 = new Entry()
{
Amount = 10F
};
Entry entry2 = new Entry()
{
Amount = 20F
};
App.Database.Insert(user);
if (user.Contacts==null)
{
user.Contacts = new List<Contact>();
}
App.Database.Insert(contact);
user.Contacts.Add(contact);
App.Database.UpdateWithChildren(user);
if (contact.Entries==null)
{
contact.Entries = new List<Entry>();
}
App.Database.Insert(entry1);
App.Database.Insert(entry2);
contact.Entries.Add(entry1);
contact.Entries.Add(entry2);
App.Database.UpdateWithChildren(contact);
App.Database.UpdateWithChildren(user);
var test = App.Database.GetWithChildren<User>(user.Id, recursive: true);
var test2 = App.Database.GetAllWithChildren<Contact>();
var test3 = App.Database.GetAllWithChildren<Entry>();
} catch (Exception ex)
{
Debug.Print(ex.Message);
}
}
I have set a breakpoint to the bracket that closes the try to inspect the result. In the End, my user looks like this:
Which is absolutely perfect.
However, when I try to get this user from my database, the result looks like this:
I don't know how to resolve this error and hope anyone can help me with this problem.
Since this post is very long already, I thank everyone who read this far in advance.
After many tries I finally solved my problem on my own.
Solution:
It could not have been any easier. In my User, Contact and Entry Class I provided my OneToMany and ManyToOne attributes with the CascadeOperation attribute. Example:
[OneToMany(CascadeOperations = CascadeOperation.All)]
public List<Entry> Entries { get; set; }
[ManyToOne(CascadeOperations = CascadeOperation.All)]
public User User { get; set; }
Even though I marked my GetWithChildren() Method as recursive: true, only by applying CascadeOperations it will work properly. More information about SQLite-Net-Extensions and CascadeOperations can be found here:
Source: TwinCoders SQLite-Net-Extensions
Started to learn asp.net and DB manipulations. Trying to implement some simple functionality - two models, one has list of references to another.
Here is an error that I currently get:
An exception occurred while initializing the database. See the InnerException for details.
Inner exception:
Unable to determine a valid ordering for dependent operations. Dependencies may exist due to foreign key constraints, model requirements, or store-generated values.
My models:
public class Killer
{
public Killer(string name, string biography)
{
Name = name;
Biography = biography;
KillerId = Guid.NewGuid();
}
public Guid KillerId { get; set; }
public string Name { get; set; }
public string Biography { get; set; }
public virtual Contract Contract { get; set; }
}
public class Contract
{
public Contract(Status status, Killer target, string description, params Killer[] targets)
{
ContractId = Guid.NewGuid();
this.status = status;
Target = target;
Description = description;
Killers = new HashSet<Killer>();
foreach (var t in targets) Killers.Add(t);
}
public Guid ContractId { get; set; }
public enum Status { active, done, failed, rejected, abandoned }
public Status status { get; set; }
public Killer Target { get; set; }
public string Description { get; set; }
//[ForeignKey("ContractID")]
public virtual ICollection<Killer> Killers { get; set; }
}
In context I initialize db with lists of objects
public class KillerContext : DbContext
{
public DbSet<Killer> Killers { get; set; }
public DbSet<Contract> Contracts { get; set; }
}
In controller I do:
KillerContext k = new KillerContext();
public ActionResult Index()
{
var contracts = k.Contracts.ToList();
ViewBag.contracts = contracts;
return View();
}
In Global.asax:
Database.SetInitializer(new KillerContextInitialization());
Here is how I enter first data in db:
public sealed class KillerContextInitialization : DropCreateDatabaseAlways<KillerContext>
{
protected override void Seed(KillerContext db)
{
List<Killer> killers = new List<Killer>();
//List<Contract> contracts = new List<Contract>();
killers.Add(new Killer(name: "Ivan Firstein", biography: "He was born in the shadows."));
killers.Add(new Killer(name: "Oleg Gazmanov", biography: "test man"));
db.Contracts.Add(new Contract(
Contract.Status.active,
killers.SingleOrDefault(x => x.Name == "Ivan Firstein"),
"KILL OR BE KILLED. As always with love.",
killers.SingleOrDefault(x => x.Name == "Oleg Gazmanov")
));
db.Killers.AddRange(killers);
base.Seed(db);
}
}
Looks like you need add ForeignKey attribute for killer Model, and store this key in property ContractId:
public class Killer
{
[ForeignKey(nameof(ContractId)] //Name of added property in line below
public Contract Contract { get; set; } //no need "virtual"
public Guid? ContractId { get; set; }
// other properties...
}
public class Contract
{
[ForeignKey("ContractId")] //Name of added property in Killer Model
public virtual ICollection<Killer> Killers { get; set; }
// other code...
}
EDIT
You should do something similar to the Contract.Target property:
[ForeignKey(nameof(TargetId)]
public Killer Target { get; set; }
public Guid TargetId { get; set; }
For enum types you should add attributes like this:
[Column(nameof(status), TypeName = "int")]
public Status status { get; set; }
Find out that problem was in public Killer Target { get; set; }
When i was adding data, that field was considered as NOT NULL, and all what i need to do, is save changes after filling killers, like so:
public sealed class KillerContextInitialization : DropCreateDatabaseAlways<KillerContext>
{
protected override void Seed(KillerContext db)
{
List<Killer> killers = new List<Killer>();
killers.Add(new Killer(name: "Ivan Firstein", biography: "He was born in the shadows."));
killers.Add(new Killer(name: "Oleg Gazmanov", biography: "test man"));
db.SaveChanges(); // - save killers first, then add them to contract
db.Contracts.Add(new Contract(
Contract.Status.active,
killers.SingleOrDefault(x => x.Name == "Ivan Firstein"),
"KILL OR BE KILLED. As always with love.",
killers.SingleOrDefault(x => x.Name == "Oleg Gazmanov")
));
db.Killers.AddRange(killers);
base.Seed(db);
}
}
I have 3 tables,
1. AttributeTypes (Columns: AttributeId (PK), AttributeName, ..)
2. Location (Columns: locationId (PK), LocationName, ...)
3. LocationAttributeType (Columns: locationId (FK), AttributeId (FK))
Whenever I am trying to insert new location record along with its attribute type from GUI, it should create new record for Table- Location and LocationAttributeType. But EF trying to add new record in Table- AttributeTypes as well, which is just used as reference table and should not add new/duplicate records in it. How can I prevent that?
here is my code,
The model which GUI sends is,
public class LocationDataModel
{
[DataMember]
public int Id { get; set; }
[DataMember]
public string Code { get; set; }
[DataMember]
public List<AttributeTypeDataModel> AssignedAttributes = new List<AttributeTypeDataModel>();
}
public class AttributeTypeDataModel
{
protected AttributeTypeDataModel() {}
public AttributeTypeDataModel(int id) { this.Id = id; }
public AttributeTypeDataModel(int id, string name)
: this(id)
{
this.Name = name;
}
[DataMember]
public int Id { get; set; }
[DataMember]
public string Name { get; set; }
[DataMember]
public virtual ICollection<LocationDataModel> Locations { get; set; }
}
The Entities created by EF are,
public partial class Location
{
public Location()
{
this.AttributeTypes = new List<AttributeType>();
}
public Location(int campusId, string code)
: this()
{
CampusId = campusId; Code = code;
}
public int Id { get; set; }
public int CampusId { get; set; }
public string Code { get; set; }
public virtual ICollection<AttributeType> AttributeTypes { get; set; }
}
public partial class AttributeType
{
public AttributeType()
{
this.Locations = new List<Location>();
}
public int AttributeTypeId { get; set; }
public string AttributeTypeName { get; set; }
public virtual ICollection<Location> Locations { get; set; }
}
I have below code to Add these new location to database,
private IEnumerable<TEntity> AddEntities<TModel, TEntity, TIdentityType>
(IEnumerable<TModel> models, Func<TModel, TIdentityType> primaryKey,
IGenericRepository<TEntity, TIdentityType> repository)
{
var results = new List<TEntity>();
foreach (var model in models)
{
var merged = _mapper.Map<TModel, TEntity>(model);
var entity = repository.Upsert(merged);
results.Add(entity);
}
repository.Save();
return results.AsEnumerable();
}
I am using following generic repository to do entity related operations
public TEntity Upsert(TEntity entity)
{
if (Equals(PrimaryKey.Invoke(entity), default(TId)))
{
// New entity
return Context.Set<TEntity>().Add(entity);
}
else
{
// Existing entity
Context.Entry(entity).State = EntityState.Modified;
return entity;
}
}
public void Save()
{
Context.SaveChanges();
}
Whats wrong I am doing here?
The DbSet<T>.Add() method attaches an entire object graph as added. You need to indicate to EF that the 'reference' entity is actually already present. There are two easy ways to do this:
Don't set the navigation property to an object. Instead, just set the corresponding foreign key property to the right value.
You need to ensure that you don't load multiple instances of the same entity into your object context. After creating the context, load the full list of AttributeType entities into the context and create a Dictionary<> to store them. When you want to add an attribute to a Location retrieve the appropriate attribute from the dictionary. Before calling SaveChanges() iterate through the dictionary and mark each AttributeType as unchanged. Something like this:
using (MyContext c = new MyContext())
{
c.AttributeTypes.Add(new AttributeType { AttributeTypeName = "Fish", AttributeTypeId = 1 });
c.AttributeTypes.Add(new AttributeType { AttributeTypeName = "Face", AttributeTypeId = 2 });
c.SaveChanges();
}
using (MyContext c = new MyContext())
{
Dictionary<int, AttributeType> dictionary = new Dictionary<int, AttributeType>();
foreach (var t in c.AttributeTypes)
{
dictionary[t.AttributeTypeId] = t;
}
Location l1 = new Location(1, "Location1") { AttributeTypes = { dictionary[1], dictionary[2] } };
Location l2 = new Location(2, "Location2") { AttributeTypes = { dictionary[1] } };
// Because the LocationType is already attached to the context, it doesn't get re-added.
c.Locations.Add(l1);
c.Locations.Add(l2);
c.SaveChanges();
}
In this specific case you are using a many-to-many relationship, with EF automatically handling the intermediate table. This means that you don't actually have the FK properties exposed in the model, and my first suggestion above won't work.
Therefore, you either need to use the second suggestion, which still ought to work, or you need to forgo the automatic handling of the intermediate table and instead create an entity for it. This would allow you to apply the first suggestion. You would have the following model:
public partial class Location
{
public Location()
{
this.AttributeTypes = new List<LocationAttribute>();
}
public Location(int campusId, string code)
: this()
{
CampusId = campusId; Code = code;
}
public int Id { get; set; }
public int CampusId { get; set; }
public string Code { get; set; }
public virtual ICollection<LocationAttribute> AttributeTypes { get; set; }
}
public partial class LocationAttribute
{
[ForeignKey("LocationId")]
public Location Location { get; set; }
public int LocationId { get; set; }
public int AttributeTypeId { get; set; }
}
public partial class AttributeType
{
public int AttributeTypeId { get; set; }
public string AttributeTypeName { get; set; }
}
With this approach you do lose functionality since you can't navigate from a Location to an AttributeType without making an intermediate lookup. If you really want to do that, you need to control the entity state explicitly instead. (Doing that is not so straightforward when you want to use a generic repository, which is why I've focused on this approach instead.)
Thank you all for your suggestions.
I have to get rid of my generic repository here to save my context changes and do it manually as below,
private IEnumerable<int> AddLocationEntities(IEnumerable<LocationDataModel> locations)
{
var results = new List<int>();
foreach (LocationDataModel l in locations)
{
var entity = _mapper.Map<LocationDataModel, Location>(l);//you can map manually also
var AttributeCode = l.AssignedAttributes.FirstOrDefault().AttributeTypeId;
using (MyContext c = new MyContext())
{
var attr = c.AttributeTypes.Where(a => a.Id == AttributeTypeId ).ToList();
entity.AttributeTypes = attr;
c.Locations.Add(entity);
c.SaveChanges();
var locid = entity.Id;
results.Add(locid);
}
}
return results;
}
In the else statement of yourUpsert you should add
context.TEntity.Attach(entity);
I have a parent/child relationship between a ProductFamily (the parent) and a list of subordinates (SaleItem). I am running the Ravendb Server locally with the server pulled up as a console app. When I query the Family data I am attempting to include the list of SaleItems in the session to avoid extra trips to the server. However on the console I see the subsequent calls to load the individual saleitem list for each family as I step through the foreach loop. I think I am doing something incorrectly and am puzzled as to what it may be. I am on day 2 of using RavenDB, so any handholding is appreciated.
Classes:
public class Family
{
public string Id { get { return string.Format(#"Families/{0}", this.FamilyID); } }
public int FamilyID { get; set; }
public string FamilyNumber { get; set; }
public string Description { get; set; }
public string[] SaleitemIds { get; set; }
public override string ToString()
{
return string.Format("Number:{0} - {1}", FamilyNumber, Description);
}
[JsonIgnore]
public List<SaleItem> SaleItems { get; set; }
}
public class SaleItem
{
public string Id { get { return string.Format(#"SaleItems/{0}", this.SaleItemID); } }
public int SaleItemID { get; set; }
public string Description { get; set; }
public override string ToString()
{
return string.Format("Number:{0} - {1}", SaleItemID.ToString(), Description);
}
}
And the code:
List<SearchTerm> searchterms = new List<SearchTerm>(){ new SearchTerm(){term="1009110922"}
,new SearchTerm(){term="1009112439"}
,new SearchTerm(){term="1009122680"}
,new SearchTerm(){term="1009124177"}
,new SearchTerm(){term="1009133928"}
,new SearchTerm(){term="1009135435"}
,new SearchTerm(){term="1009148000"}};
using (IDocumentSession session = documentStore.OpenSession())
{
var results = session.Query<Family>().Customize(o => o.Include<SaleItem>(s => s.Id)).Where(x => x.FamilyNumber.In(searchterms.Select(t => t.term).ToList()));
foreach (Family fam in results)
{
Console.WriteLine(fam.ToString());
fam.SaleItems = session.Load<SaleItem>(fam.SaleitemIds).ToList();
foreach (SaleItem si in fam.SaleItems)
{
Console.WriteLine(" " + si.ToString());
}
}
}
As I step through the code I see the calls to Get the list of saleitems on the line:
fam.SaleItems = session.Load<SaleItem>(fam.SaleitemIds).ToList();
I believe I have implemented something incorrectly, but I am new enough with this platform to accept that I could have simply misunderstood what the behavior would be. There are definitely cases where I do not want the Saleitem doc to be embedded in the Family doc, so that is not really an option in this case.
Doug_w,
Look at what you are including:
o.Include<SaleItem>(s => s.Id)
You probably want it to be:
o.Include<SaleItem>(s => s.SaleitemIds )