Linq search database considering duplicates - database

I've got the problem with searching database through duplicate list values.
First I search all occurrences by given string.
var parentIdList = await _context.ECATEGORIES
.Where(a => a.NAME.ToLower().Contains(partOfName.ToLower()))
.ToListAsync(ct);
then
I retrieve all names when given PARENTID of parentIdList equals database ID
var mainName = await _context.ECATEGORIES
.Where(a=> parentIdList.Any(p=>p.PARENTID==a.ID) )
.Select(s => s.NAME)
.ToListAsync(ct);
My problem is that, sometimes property PARENTID is duplicated.
For example PARENTID = {1,1,2,2,4,5,6}
then result is mainName = {"a","b","c","d","e"}
But I want mainName = {"a", "a","b","b","c","d","e"}

So you have a sequence of Categories, where every Category has an Id, a ParentId and a Name. You also have a string partOfName
You want the Names of Categories. All Categories? No. only those Categories that have a Parent which has a Name that looks like partOfName (looks like is represented by the use of the function contains in your code)
I'm not sure if you use entity framework. Your use of _context seems a hint to this. In that case, It would be easier to use the virtual parent properties. See later.
If you do not use entity framework, I'd advise to fetch your data in one query: join the elements with their parents, and keep only the join results where the parent meets your contains predicate:
string lowerCasePartOfName = partOfName.ToLower(); // for efficiency: do this only once
IQueryable<Category> categories = myDbContext.Categories;
IQueryable<Category> validParents = myDbContext.Categories
.Where(category => category.Name.ToLower().Contains(lowerCasePartOfName))
Now join every Category with the validParentIds on Category.ParentId = Parent.Id
var query = Categories.Join(validParents, // join categories with validParents
category => category.ParentId, // from each category take the ParentId
parent => parent.Id, // from each valid parent take the Id
(category, parent) => new // when they match make one new object
{
CategoryName = category.Name,
});
Note, until now the query has been made, it has not been executed yet. If you want you can concatenate them into one big LINQ query. I'm not sure if that would increase performance very much; however, it would decrease readability.
var result = query.ToListAsync();
Entity Framework Solution
If you use entity framework, your Category class would be like:
class Category
{
public int Id {get; set;}
public string Name {get; set;}
// every Category has zero or one Parent category:
public int? ParentId {get; set;}
public Category Parent {get; set;}
// every Category has zero or more Children, each of them has this Category as Parent
public virtual ICollection<Category> Children {get; set;}
}
You query will be much simpler:
Give me all names of Categories whose Parents have a name that looks like partOfName
string lowerCasePartOfName = partOfName.ToLower();
var result = myDbContext.Categories
// keep only the Categories whose name looks like:
.Where(category => category.Parent.Name.ToLower().Contains(lowerCasePartOfName))
.Select(category => category.Name);

Related

Criteria API find Key-Value Pairs in Map

I try to use the Criteria API to create a dynamic JPA-Query. I need to find a key-value pair inside a map of the object.
The Object looks similar to the following one.
public class item {
private UUID id;
#Column(name = "properties", columnDefinition = "nvarchar")
private Map<String, Object> properties;
}
I thought I could use the MapJoin join or joinMap:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Item> criteriaQuery = cb.createQuery(Item.class);
Root<Item> itemRoot = criteriaQuery.from(Item.class);
criteriaQuery.select(itemRoot);
Join<String, Object> properties = itemRoot.join("properties");
// or
//MapJoin<Item, String, Object> properties = itemRoot.joinMap("properties");
Predicate pre1 = cb.equal(properties.get(ITEM_PROPERTY_1), "123");
Predicate pre2 = cb.equal(properties.get(ITEM_PROPERTY_2), "456");
Predicate propertiesPredicate = cb.and(pre1, pre2);
criteriaQuery.where(propertiesPredicate);
Item item = em.createQuery(criteriaQuery).getSingleResult();
But I've read that this is only for associations.
On the join i get an:
IllegalArgumentException: Requested attribute was not a map.
So could sb explain to me, how I will be able to find a key-value pair in a map with the Criteria API?
Edit: I am not able to change anything in the DB.
So I need to guess a little bit because you didn't show us your DB Table, that's why I answer a little bit more freely.
And as a disclaimer: it might be easier and it would be more efficient to query a real table instead of an serialized object/json.
But I would do it this way:
The Table in MSSQL:
create table ${schema}.myTable
(
id bigint identity
constraint PK_myStuff
primary key,
properties nvarchar(max) not null
) go
The Java entity (draft):
public class Item extends AbstractPersistable<...> {
#Column(name = "properties", columnDefinition = "nvarchar")
private String properties;
}
The Java Specification:
protected Specification<Item> customFilter(String filterArg) {
return (root, query, cb) ->
cb.like(root.get(Item_.properties), filterArg);
}
This way your query searches the properties for a string pattern.
Info:
https://vladmihalcea.com/sql-server-json-hibernate/

SQLKata return complex object with Include/IncludeMany

I need to return from a query a list of students where each students has assignments.
class Student {
public string Name{ get; set; }
public IEnumerable<Assignment> Assignments{ get; set; }
}
class Assignment {
public string Title { get; set; }
}
so tried to create two queries and then use include/includemany but i get other errors (The given key 'Id' was not present in the dictionary)
var students = db.Query('Student')
.Select(...)
.Where()
var assignments= db.Query('Assignment')
.Select(...)
.Where()
var result = students.IncludeMany('Assignments', assignments).Get()
but this does not work. How can you properly create complex/nested objects with SQLKata?
I asume your example is complete, but you should add the primairy and foreign key properties on the queries. (They are not in your classes so I added them based on conventions sql kata uses)
var students = db.Query("Student")
.Select("Id", "Name");
var assignments = db.Query("Assignment")
.Select("StudentId", "Title");
var result = students
.IncludeMany("Assignments", assignments)
.Get();
If they are not specified as an argument to the IncludeMany method, SqlKata will use the "Id" field as primary key and the "StudentId" field as the foreign key in the relationship. You can add these as extra arguments to the IncludeMany method.
var result = students
.IncludeMany("Assignments", assignments, "StudentId", "Id")
.Get();
Please note that at this moment SqlKata doesn't seem to support this on the generic method Get<> GetAsync<>.

SQL query to Lambda Expression C#

SQL query:
SELECT M.MailItemId, m.Subject, ISNULL(m.SendCC, ''), ISNULL(attachments.counter, 0) Counters, M.CreationDate
FROM MailItem AS M
LEFT OUTER JOIN (
SELECT MailItemId, COUNT(MailItemId) counter
FROM Attachment Group By MailItemId
) AS attachments ON M.MailItemId = attachments.MailItemId
There are two models MailItem and Attachment.
MailItemId is primary key in MailItem and foreign key in Attachment model.
Want to convert above query to a lambda expression.
So you have a table of MailItems and a table of Attachments. There is a straightforward one-to-many relation between MailItems and Attachments: every MailItem has zero or more Attachments, and every Attachment belongs to exactly one MailItem, namely the MailItem that the foreign key refers to.
Quite often, people want "MailItems with their Attachments", they don't want a left outer join:
MailItem 1 with Attachments A, C
MailItem 2 with Attachments B, D, F
MailItem 3 without Attachments
MailItem 4 with Attachment E
You on the other hand, seem to prefer the following result:
MailItem Attachment
1 A
2 B
1 C
3 <null>
4 E
2 D
2 F
This solution is seldom wanted. That's why there is no standard LINQ method for it.
To get MailItems with their Attachment, use GroupJoin. If you want the left outer join, use SelectMany after the GroupJoin.
MailItems with their Attachments
var mailItemsWithTheirAttacments = dbContext.MailItems.GroupJoin(
dbContext.Attachments, // GroupJoin MailItems with Attachments
mailitem => mailItem.MailItemId, // from every MailItem take the primary key
attachment => attachment.MailItemId, // from every Attachment take the foreign key
// parameter resultSelector: for every MainItem and all its Attachments
// make one new object
(mailItem, attachmentsOfThisMailItem) => new
{
// Select the MailItem properties that you plan to use
Id = mailItem.Id,
Sender = mailItem.Sender,
...
Attachments = attachmentsOfThisMailItem.Select(attachment => new
{
// Select only the Attachments properties that you plan to use:
Id = attachment.Id,
Format = attachment.Format,
...
// not needed, you already know the value:
// MailItemId = attachment.MailItemId,
})
.ToList(),
})
Left Outer Join
var result = dbContext.MailItems.GroupJoin(dbContext.Attachments,
mailitem => mailItem.MailItemId,
attachment => attachment.MailItemId,
(mailItem, attachmentsOfThisMailItem) => new
{
MailItem = mailItem,
Attachments = attachmentsOfThisMailItem,
})
// make it a left outer join, using SelectMany
.SelectMany(groupJoinResult => groupJoinResult.Attachments,
(groupJoinResult, attachment) => new
{
MailId = groupJoinResult.MailItem.Id,
Sender = groupJoinResult.MailItem.Sender,
...
AttachmentId = attachment.Id,
Format = attachment.Format,
...
});
Use the virtual ICollections
If you have followed the entity framework conventions, you will have classes similar to the following:
class MailItem
{
public int Id {get; set; }
...
// every MailItem has zero or more Attachments (one-to-many)
public virtual ICollection<Attachment> Attachments {get; set;}
}
class Attachment
{
public int Id {get; set; }
...
// every Attachment belongs to exactly one MailItem, using foreign key:
public int MailItemId {get; set;}
public virtual MailItem MailItem {get; set;}
}
You might use other identifiers, but the important part are the virtual properties.
In entity framework the columns of the tables are represented by the non-virtual properties. The virtual properties represent the relations between the tables (one-to-many, many-to-many, ...)
The foreign key is a column in table Attachments. Hence property Attachment.MailItemId is not virtual. Property Attachment.MailItem represent the relation, hence it is virtual.
If you have defined the relations as properties, you can use them to do the GroupJoin or the Left Outer Join for you:
var mailItemsWithTheirAttachments = dbContext.MailItems.Select(mailItem => new
{
Id = mailItem.Id,
Sender = mailItem.Sender,
...
Attachments = mailItem.Attachments
.Where(attachment => ....) // only if you don't want all Attachments
.Select(attachment => new
{
Id = attachment.Id,
...
})
.ToList(),
});
Entity framework knows the relation, and will do the correct (group-)join for you.
IMHO this looks way more natural than a GroupJoin, whether or not followed by a selectMany

Dapper multi-mapping not returning null object when splitOn column is not in child object

I'm using dapper 1.50.2 with MySQL and running into a problem trying to map a left outer join child object to its parent. If I split on a column alias that doesn't actually exist in the child object, Dapper always creates a child object with default properties, even when there is nothing in the left join.
I created a simple example to demonstrate this:
public class ParentRecord
{
public string MemberID { get; set; }
public ChildRecord Child { get; set; }
}
public class ChildRecord
{
//public string Split { get; set; }
public string SomeField { get; set; }
}
using (MySqlConnection connection = new MySqlConnection(connectionString))
{
ParentRecord result = connection.Query<ParentRecord, ChildRecord, ParentRecord>(
#"SELECT 'FakeID' AS MemberID, NULL AS Split, NULL AS SomeField",
(mt, crt) =>
{
mt.Child = crt;
return mt;
},
splitOn: "Split").Single();
}
I would expect this to result a ParentRecord with the Child property set to null, but the Child property is set to a ChildRecord with all default fields.
If I uncomment the Split property in ChildRecord, or if I split on SomeField, this works as I'd expect.
Are there any good workarounds for this?
In the actual query I'm dealing with, there are multiple primary key and foreign key fields with the same names and I'd rather not change the property names in the POCOs to be unique. I'd prefer to be able to use column aliases that are just there to split on. I know this isn't normally how Dapper is set to up to work.
Any help would be appreciated, thanks.
This happen because the object Child initialize for default when you attribute the ctr param. Then the solution that I did implement was:
ParentRecord result = connection.Query<ParentRecord, ChildRecord, ParentRecord>(
#"SELECT 'FakeID' AS MemberID, NULL AS Split, NULL AS SomeField",
(mt, crt) =>
{
if (crt.SomeField != null){ mt.Child = crt; }
return mt;
},
splitOn: "Split").Single();

Get Paginated SQL Server Result AND total count in one query via Dapper?

If I had a User model that looks like this:
public class User
{
public Guid Id { get; set; }
public DateTime CreatedAtUtc { get; set; }
public string Username { get; set; }
public string Country { get; set; }
}
...and I'd perform a query which starts at a given row and fetches a limited amount of further rows (basically for paginating over the results) that looks like this:
var spaniards = connection.Query<User>(
"select * from Users where Country=#Country OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY",
new { Country = "Spain" }).ToList();
.. using Dapper(.Net), would it be possible to get that particular, limited result set AND the total count of rows in one single query and if so.. how?
One way to do solve this is using the splitOn functionality, together with count(1) over()
Let's assume the following sql string:
var sql = "select *, overall_count = COUNT(1) OVER() from Users ORDER BY Id Asc OFFSET 5 ROWS;"
together with the following dapper.net call:
HashSet<int> hashSet = new HashSet<int>();
Func<User, int, User> map = (result, count) =>
{
hashSet.Add(count);
return result;
};
await connection.QueryAsync(sql, map, "overall_count").ConfigureAwait(false);
This will split the result of the dapper call into two parts (just like with joining different tables) and it will call the map functor each time, in this case we simply store the count in a hashset (you could then check if the hashSet.Count is 1)
Hope this helps
PS: I'm not certain that your OFFSET works without any ORDER BY

Resources