I want to update the itemsToUpdate collection.
This collection is already used in a query thus the resulting entities are already tracked in the context local property.
What is the most efficient way of overriding properties of the context.items.Local property from the itemsToUpdate collection?
private async Task<IEnumerable<item>> GetitemsAsync(IEnumerable<item> itemIds)
{
return await context.items.Where(t => itemIds.Select(x => x.Id).Contains(t.Id)).ToListAsync();
}
public async Task Update(...)
{
// Update
var queryUpdateitems = await GetitemsAsync(itemsToUpdate);
bool canUpdate = queryUpdateitems.All(t => t.UserId == userId);
if (!canUpdate)
{
throw new NotAuthorizedException();
}
else
{
// update here the itemsToUpdate collection
}
context.SaveChangesAsync();
}
In your case, you know that you have to update all these items, you just want to make sure that current user can update all items (by comparing Item.UserId). Instead of fetching all the existing items from database to make the check, you can query database to give result of the check and then you can just send update to database if check is true.
var itemIds = itemsToUpdate.Select(x => x.Id).ToList();
var canUpdate = await db.Blogs.Where(b => itemIds.Contains(b.Id)).AllAsync(t => t.UserId == userId);
if (canUpdate)
{
db.UpdateRange(itemsToUpdate);
}
else
{
throw new NotSupportedException();
}
await db.SaveChangesAsync();
Here, you have to make list of itemIds first because EF cannot inline list of items in a query and will do evaluation on client otherwise. That means EF is fetching whole table. Same is true for your GetitemsAsync method. It also queries whole table. Consider creating itemIds locally in that method too.
Once you pass in List<int> in the method EF will be happy to inline it in query and for query of canUpdate it will sent single query to database and fetch just true/false from database. Then you can use UpdateRange directly since there are nomore tracking records. Since it does not fetch all items from database, it will be faster too.
Related
I have a collection with the following schema:
const CategorySchema = Schema({
name: String,
order: Number,
});
I'm trying to update the order field of the categories. The why I'm planning to do it is to have a local array with the ids of the categories in the order I want. Then, I'd fetch all categories (they are not many), and I'd start looping over the local array of ids. For each id, I'll locate it in the fetched array, and update the order according to the index of that id in the local array. The issue now is how to save it. Below is what I'm trying to do:
// Get all categories.
const categories = await Category.find({}, 'order');
console.log(categories);
// Get the order from the request.
const orderedItemIds = req.body.itemIds || [];
orderedItemIds.forEach((id, idx) => {
categories.find(x => x._id === id).order = idx;
});
// Save.
try {
await categories.save();
res.sendStatus(200);
} catch (e) {
console.log(e);
res.sentStatus(423);
}
When you query your categories, mongoose by default returns an array of instances of the Mongoose Document class. That means you can call their save() method whenever you mutate them.
So you can save your docs immediately after you assign the idx variable:
const orderedItemIds = req.body.itemIds || [];
orderedItemIds.forEach((id, idx) => {
const cat = categories.find(x => x._id.toString() === id);
cat.order = idx;
cat.save();
});
Note a few things about this code.
I assume that req.body.itemIds is a array of strings representing ObjectIds (e.g. '602454847756575710020545'). So In order to find a category in categories, you will need to use the .toString() method of the x._id object, because otherwise you will be trying to compare an Object and a string, which will never be true.
You can save the category right after assigning idx to cat.order without having to await it, because the next update is not depending on the save status of the previous.
The DbContext DbSet<T>.Load / DbSet<T>.LoadAsync methods return void and Task respectively: they execute queries and then add the loaded/instantiated entity objects into the DbContext's DbSet and update the navigation properties and reverse-navigation of already-loaded objects, but they don't return any information about what they loaded: there doesn't seem to be a way of getting the actual count of the number of rows that were loaded.
Which is surprising, considering that the SaveChanges / SaveChangesAsync method does return the number of rows affected by any DML statements it executes.
I know there's a workaround in that I could use ToList/ToListAsync instead and then use the List<T>.Count property, but that's defeating the point of using Load/LoadAsync.
For example, consider this two-step query operation:
async Task<PageViewModel> LoadOrdersAsync( Int32 customerId, Expression<Func<Order,Boolean>> predicate )
{
// Step 1:
List<Order> orders = await this.dbContext.Orders
.Where( o => o.CustomerId == customerId )
.Where( predicate )
.ToListAsync()
.ConfigureAwait(false);
// Step 1.5:
List<Int32> orderIds = orders.Select( o => o.OrderId ).ToList();
// Step 2:
await this.dbContext.OrderItems
.Where( i => orderIds.Contains( i.OrderId ) )
.LoadAsync()
.ConfigureAwait(false);
// Done!
return new PageViewModel( orders );
}
I want to get the quantity of OrderItem entities that were loaded in the second step, but as far as I know that isn't possible without using ToList/ToListAsync.
You’re right, there is no easy way to get the number of loaded entries of the Load. It is essentially the same as ToList without creating the list and adding the loaded elements to it. If you really don’t want to use ToList, one option is to access the DbContext.ChangeTracker and get the number of entries from that:
var entriesBefore = context.ChangeTracker.Entries().Count();
// load more entities
var loaded = context.ChangeTracker.Entries().Count() - entriesBefore;
Note, that this is not accurate when you include other, related entities in your query.
I am lambda querying models (I make projection with other classes-GameBankVM, GameCouponBankVM) and at the end, I would like to loop throuh query result and update the model field. But I am getting The entity or complex type 'EPINMiddleWareAPI.Models.GameBankVM' cannot be constructed in a LINQ to Entities query.
Here is my sample code:
var gameBankResult = await (context.GameBanks.Where(g => g.productCode == initiate.productCode)
.Take(initiate.quantity)
.Select(g => new GameBankVM
{
quantity = g.quantity,
currency = g.currency,
initiationResultCode = g.initiationResultCode,
productCode = g.productCode,
productDescription = g.productDescription,
referenceId = g.referenceId,
responseDateTime = g.responseDateTime,
unitPrice = g.unitPrice,
totalPrice = g.totalPrice,
coupons = g.coupons.Select(c => new GameCouponBankVM
{
Pin = c.Pin,
Serial = c.Serial,
expiryDate = c.expiryDate
}).ToList()
})).ToListAsync();
if (gameBankResult.Count() != 0)
{
foreach (var item in gameBankResult)
{
item.referenceId = initiate.referenceId;
context.Entry(item).State = System.Data.Entity.EntityState.Modified;
}
await context.SaveChangesAsync();
return Ok(gameBankResult);
}
How can I update referenceId on my GameBank model/table?
In this scenario, your data won't be updated because your query is returning a List of GameBankVM and not a List of GameBank, now technically speaking, you are breaking SRP, you should either update your data or query your data not both in the same method, you may want to refactor your method like this :
1.- Create a private method for data update, in this case, you query directly GameBank iterate thru list entries, make your changes and save them to the database, this same method can return List of GameBank to avoid another database roundtrip.
2.- In the controller after you call your new method, you can run the transformation query to convert List of GameBank to List of GameBankVM and return it to the view.
There are many other ways to do this, I'm just recommending this as a less impact way to make your controller work. But if you are willing to make things better, you can create a business layer where you resolve all your business rules, or you can use patterns like CQS or CQRS.
I am using postgresql, knex, and bookshelf to make queries to update my users table. I would like to find all users who didn't sign in during a specific time and then update their numAbsences and numTardies field.
However it appears that when running a raw sql query using bookshelf.knex the result that I get for users is an array of objects rather than an array of bookshelf objects of objects because I can't save the objects directly to the database when I try to use .save(). I get the exception user.save is not a function.
Does anyone know how I can update the values in the database for the users? I've seen the update function but I need to also return the users in absentUsers so I select them currently.
// field indicates whether the student was late or absent
var absentUsers = function(field){
// returns all users who did not sign in during a specific time
if (ongoingClasses){
return bookshelf.knex('users')
.join('signed_in', 'signed_in.studentId', '=', 'users.id')
.where('signed_in.signedIn', false)
.select()
.then(function(users){
markAbsent(users, field);
return users;
});
}
}
var markAbsent = function(users, field){
users.forEach(function(user){
user[field]++;
user.save();
})
}
I've solved my problem by using another sql query in knex. It seemed there was no way to use a sql query and then use standard bookshelf knex methods since the objects returned were not bookshelf wrapper objects.
var absentUsers = function(field){
// returns all users who did not sign in during a specific time
if (ongoingClasses){
return bookshelf.knex('users')
.join('signed_in', 'signed_in.studentId', '=', 'users.id')
.where('signed_in.signedIn', false)
.select()
.then(function(users){
markAbsent(users, field);
});
}
}
var markAbsent = function(users, field){
users.forEach(function(user){
var updatedUser = {};
updatedUser[field] = user[field]+1;
bookshelf.knex('users')
.where('users.id', user.id)
.update(updatedUser).then(function(){
});
});
}
With your code bookshelf.knex('users') you leave the "Bookshelf world" and are in "raw knex world". Knex alone doesn't know about your Bookshelf wrapper objects.
You may use Bookshelf query method to get the best of both worlds.
Assuming your model class is User, your example would look approximately like
User.query(function(qb) {
qb.join('signed_in', 'signed_in.studentId', 'users.id')
.where('signed_in.signedIn', false);
})
.fetchAll()
.then(function(bookshelfUserObjects) {
/*mark absent*/
return bookshelfUserObjects.invokeThen('save'); // <1>
});
<1> invokeThen: Call model method on each instance in collection
I want to fetch the candidate and the work exp where it is not deleted. I am using repository pattern in my c# app mvc.
Kind of having trouble filtering the record and its related child entities
I have list of candidates which have collection of workexp kind of throws error saying cannot build expression from the body.
I tried putting out anonymous object but error still persist, but if I use a VM or DTO for returning the data the query works.
It's like EF doesn't like newing up of the existing entity within its current context.
var candidate = dbcontext.candidate
.where(c=>c.candiate.ID == id).include(c=>c.WorkExperience)
.select(e=>new candidate
{
WorkExperience = e.WorkExperience.where(k=>k.isdeleted==false).tolist()
});
Is there any workaround for this?
You cannot call ToList in the expression that is traslated to SQL. Alternatively, you can start you query from selecting from WorkExperience table. I'm not aware of the structure of your database, but something like this might work:
var candidate = dbcontext.WorkExperience
.Include(exp => exp.Candidate)
.Where(exp => exp.isdeleted == false && exp.Candidate.ID == id)
.GroupBy(exp => exp.Candidate)
.ToArray() //query actually gets executed and return grouped data from the DB
.Select(groped => new {
Candidate = grouped.Key,
Experience = grouped.ToArray()
});
var candidate =
from(dbcontext.candidate.Include(c=>c.WorkExperience)
where(c=>c.candiate.ID == id)
select c).ToList().Select(cand => new candidate{WorkExperience = cand.WorkExperience.where(k=>k.isdeleted==false).tolist()});