RavedDB "outerjoin" - ravendb

I have 2 collections of documents in my ravenDB, location and address like this:
public class Location()
{
public string Id{get;set;}
public string Name{get;set;}
public List<Address> Addresses{get;set;}
}
public class Address()
{
public string Id{get;set;}
public string StreetName {get;set;}
public string PostalCode {get;set;}
}
So a Location can have zero og many addresses. And I can have many addresses that are not assigned to a Location.
If I have data like this:
//2 Location document
{
Id:"Location/1",
Name: "My Location 1"
Addresses: [{id:"Address/1"},
{id:"Address/2"}
]
}
{
Id:"Location/2",
Name: "My Location 2"
Addresses: [
{id:"Address/2"}
]
}
//3 Address document
{
Id: "Address/1",
StreetName: "Street1 10",
PostalCode: "1000"
}
{
Id: "Address/2",
StreetName: "Street1 11",
PostalCode: "1000"
}
{
Id: "Address/3",
StreetName: "Street1 12",
PostalCode: "2000"
}
I'm struggleing to find the best way to create an index of this which will give me a resultset like this:
StreetAddress PostalCode Location
Street1 10 1000 My Location 1
Street2 11 1000 My Location 2
Street2 11 1000 My Location 1
Street3 12 2000
I would appreciate any input on this:-)

One option is to model the other way around, i.e.
public class Location()
{
public string Id {get;set;}
public string Name {get;set;}
}
public class Address()
{
public string Id {get;set;}
public string StreetName {get;set;}
public string PostalCode {get;set;}
// Locations is a list with the Location documents IDs
public List<string> Locations {get;set;}
}
And then the index can be:
from address in docs.Addresses
select new {
StreetAddress = address.StreetName,
PostalCode = address.PostalCode
}
And then use the following Query:
from index 'AddressesIndex' as x
load x.Locations as locations[]
select {
StreetName: x.StreetName,
PostalCode: x.PostalCode,
Locations: locations.map(x => x.Name)
}
===========================================================================
A second option is to use the same models as you have in the question description
and define a multi-map-reduce index.
The multi-map-reduce index will be:
The first map:
from locationDoc in docs.Locations
from addressId in locationDoc.Addresses
let address = LoadDocument(addressId, "Addresses")
select new {
StreetName = address.StreetName,
PostalCode = (string)null,
LocationNames = new []{locationDoc.Name},
}
The second map:
from address in docs.Addresses
select new{
StreetName = address.StreetName,
PostalCode = address.PostalCode,
LocationNames = (string)null
}
The reduce:
from result in results
group result by new { result.StreetName } into g
select new {
StreetName = g.Key.StreetName,
PostalCode = g.Select(x => x.PostalCode).FirstOrDefault(x => x != null),
LocationNames = g.SelectMany(x => x.LocationNames)
}

Related

IQueryable not generating the desired query in entity framework core

I have some entities similar to the following:
public class Teacher
{
public int TeacherId { get; set; }
public string TeacherName { get; set; }
public int Age { get; set; }
public string CurrentGrade { get; set; }
public int YearsTeaching { get; set; }
pubilc ICollection<StudentFeedback> StudentFeedback { get; set; }
}
public class StudentFeedback
{
public int StudentFeedBackId { get; set; }
public int TeacherId { get; set; }
public int StudentId { get; set; }
public string Feedback { get; set; }
public Teacher Teacher { get; set; }
public Student Student { get; set; }
}
public class Student
{
public int StudentId { get; set; }
public string StudentName { get; set; }
public int Age { get; set; }
public string CurrentGrade { get; set; }
}
I have a repository with a method where I want to return a teacher or list of teachers where the StudentFeedback returned belongs to the student who is looking at it (the studentId is stored in a token).
So, lets say I have the Teacher (teacherId) and a Student (userId) who is hitting the API endpoint. I currently have the following:
int teacherId = 2;
int userId = 20; // This is the currently logged in user, extracted from the token.
var query = _context.Teachers.AsQueryable();/* _context is the DataContext*/
query = query.Where(t => p.TeacherId == teacherId);
query = query.Where(u => u.StudentFeedback.Any(x => x.StudentId == userId));
However this is still returning all StudentFeedback from all Students, so long as the userId (student) has feedback provided for the teacher in question. I had a look at the query that gets executed and the problem is that the studentId predicate is in the wrong place. A very rough version of the query is:
SELECT *
FROM ( SELECT t.*
FROM dbo.Teachers t
WHERE (t.TeacherId = 2)
AND EXISTS ( SELECT 1
FROM dbo.StudentFeedback t0
WHERE (t.TeacherId = t0.TeacherId)
AND (t0.StudentId = 20))) p
LEFT JOIN dbo.StudentFeedback sf ON p.TeacherId = sf.TeacherId
Whereas it should be something like
SELECT *
FROM ( SELECT t.*
FROM dbo.Teachers t
WHERE (t.TeacherId = 2)) p
LEFT JOIN dbo.StudentFeedback sf ON p.TeacherId = sf.TeacherId
AND sf.StudentId = 20
but I don't know how to make that happen. Is there something wrong with the IQueryable predicates I've setup or have I missed some logic in the modelBuilder within the datacontext?
Thank-you.
Edit: I am using Entity Framework Core 5.0.2 and I am also using Automapper with the following code:
query.ProjectTo<TeacherDTO>(_mapper.ConfigurationProvider).AsNoTracking()
Here is what I am getting back currently:
[
{
"teacherid": 2,
"teacherName": "Jane Smith",
"age": 35,
"currentGrade": "One",
"yearsTeaching": 12,
"studentFeedback": [
{
"studentFeedBackId": 12,
"teacherId": 6,
"studentId": 20,
"feedback": "Ms Smith is my favorite teacher"
} ,
{
"studentFeedBackId": 16,
"teacherId": 6,
"studentId": 43,
"feedback": "Ms Smith was so kind to me"
} ,
{
"studentFeedBackId": 21,
"teacherId": 6,
"studentId": 89,
"feedback": "Thank you Mrs Smith for being my teacher. I learned a lot."
}
]
}
]
here is what I want to be getting back:
[
{
"teacherid": 2,
"teacherName": "Jane Smith",
"age": 35,
"currentGrade": "One",
"yearsTeaching": 12,
"studentFeedback": [
{
"studentFeedBackId": 12,
"teacherId": 6,
"studentId": 20,
"feedback": "Ms Smith is my favorite teacher"
}
]
}
]
Thanks #LucianBargaoanu for pointing me in the right direction by saying to have the where in the mapping itself. The solution is to use Parameterization when using Automapper:
The code from this pages shows an example:
string currentUserName = null;
cfg.CreateMap<Course, CourseModel>()
.ForMember(m => m.CurrentUserName, opt => opt.MapFrom(src => currentUserName));
and then
dbContext.Courses.ProjectTo<CourseModel>(Config, new { currentUserName = Request.User.Name });
If you want a join you should use Join method in your linq statement. See https://www.tutorialsteacher.com/linq/linq-joining-operator-join . You got exactly what you wrote in your query. Where(u => u.StudentFeedback.Any(x => x.StudentId == userId));
.Any translates to exists.
If you use Net5 EF you can just add a Students property to the Teacher class:
public class Teacher
{
.....
pubilc ICollection<Student> Students { get; set; }
pubilc ICollection<StudentFeedback> StudentFeedbacks { get; set; }
}
You can use query this way:
var query = _context.Teachers.Include(i=> i.StudentFeedbacks)
.Where(t =>
t.TeacherId == teacherId
&& t.StudentFeedbacks.Any(x => x.StudentId == userId))
.ToArray();

Entity Framework Core OwnsOne unnecessary JOINS on same table

Entity framework core seems to create very complex queries when configuring value objects with OwnsOne on the same table.
Entity: Restaurant.cs
public class Restaurant : Entity, IAggregateRoot
{
public RestaurantId Id { get; private set; }
public string Name { get; private set; }
public Address Address { get; private set; }
protected Restaurant()
{
}
public Restaurant SetAddress(double lat, double lng, string street, string location, string zip, string country)
{
Address = new Address(lat, lng, street, location, zip, country);
return this;
}
}
Value object: Address.cs
public class Address : ValueObject
{
public Point Location { get; set; }
public string Street { get; private set; }
public string City { get; private set; }
public string Country { get; private set; }
public string Zip { get; private set; }
private Address() { }
public Address(double lat, double lng, string street, string city, string zip, string country)
{
Location = NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326).CreatePoint(new Coordinate(lng, lat));
Street = street;
City = city;
Country = country;
Zip = zip;
}
protected override IEnumerable<object> GetAtomicValues()
{
yield return Street;
yield return City;
yield return Country;
yield return Zip;
}
}
EF Config: RestaurantEntityTypeConfig.cs
internal class RestaurantEntityTypeConfig : IEntityTypeConfiguration<Restaurant>
{
public void Configure(EntityTypeBuilder<Restaurant> builder)
{
builder.ToTable("Restaurant", SchemaNames.Restaurant);
builder.HasKey(c => c.Id);
builder.OwnsOne(c => c.Address, c=>
{
c.WithOwner();
});
}
}
Repository method:
public virtual async Task<T> FirstOrDefaultAsync(Expression<Func<T, bool>> predicate, params public virtual async Task<T> FirstOrDefaultAsync(Expression<Func<T, bool>> predicate, params Expression<Func<T, object>>[] includeProperties)
{
var query = _dbContext.Set<T>().Where(predicate);
query = includeProperties.Aggregate(query, (current, includeProperty) => current.Include(includeProperty));
return await query.FirstOrDefaultAsync();
}
The query when selecting through repository looks like this:
SELECT TOP(1) [r].[Id], [r].[Name], [t].[Id], [t].[Address_City], [t].[Address_Country], [t].[Address_State], [t].[Address_Street], [t].[Address_ZipCode]
FROM [restaurant].[Restaurant] AS [r]
LEFT JOIN (
SELECT [r0].[Id], [r0].[Address_City], [r0].[Address_Country], [r0].[Address_State], [r0].[Address_Street], [r0].[Address_ZipCode], [r1].[Id] AS [Id0]
FROM [restaurant].[Restaurant] AS [r0]
INNER JOIN [restaurant].[Restaurant] AS [r1] ON [r0].[Id] = [r1].[Id]
WHERE [r0].[Address_ZipCode] IS NOT NULL OR ([r0].[Address_Street] IS NOT NULL OR ([r0].[Address_State] IS NOT NULL OR ([r0].[Address_Country] IS NOT NULL OR [r0].[Address_City] IS NOT NULL)))
) AS [t] ON [r].[Id] = [t].[Id]
WHERE [r].[Id] = #__p_0
The sql table looks like this:
My expected result would be a flat query since the value object is persisted to the same sql table. Is this a bug of entity framework or am I missing a configuration?

Can I use an index as the source of an index in RavenDB

I'm trying to define an index in RavenDb that uses the output of another index as it's input but I can't get it to work.
I have the following entities & indexes defined.
SquadIndex produces the result I expect it to do but SquadSizeIndex doesn't even seem to execute.
Have I done something wrong or is this not supported?
class Country
{
public string Id { get; private set; }
public string Name { get; set; }
}
class Player
{
public string Id { get; private set; }
public string Name { get; set; }
public string CountryId { get; set; }
}
class Reference
{
public string Id { get; set; }
public string Name { get; set; }
}
class SquadIndex : AbstractIndexCreationTask<Player, SquadIndex.Result>
{
public SquadIndex()
{
Map = players => from player in players
let country = LoadDocument<Country>(player.CountryId)
select new Result
{
Country = new Reference
{
Id = country.Id,
Name = country.Name
},
Players = new[]
{
new Reference
{
Id = player.Id,
Name = player.Name
}
}
};
Reduce = results => from result in results
group result by result.Country
into g
select new Result
{
Country = g.Key,
Players = g.SelectMany(x => x.Players)
};
}
internal class Result
{
public Reference Country { get; set; }
public IEnumerable<Reference> Players { get; set; }
}
}
class SquadSizeIndex : AbstractIndexCreationTask<SquadIndex.Result, SquadSizeIndex.Result>
{
public SquadSizeIndex()
{
Map = squads => from squad in squads
select new Result
{
Country = squad.Country,
PlayerCount = squad.Players.Count()
};
Reduce = results => from result in results
group result by result.Country
into g
select new Result
{
Country = g.Key,
PlayerCount = g.Sum(x => x.PlayerCount)
};
}
internal class Result
{
public Reference Country { get; set; }
public int PlayerCount { get; set; }
}
}
No, you can't. The output of indexes are not documents to be indexed.
You can use the scripted index results to chain indexes, but that isn't trivial.

How to code ViewModel with Key for human-readable values instead of Ids

I am getting an error when I perform an Update-Database in PM Console. I understand why, but given my needs I'm not sure how to best overcome it. I hope somebody can point me in the right direction.
I have 2 tables, one for countries and one for states. Usually I would use the Id for a primary key, but my client has asked that I make associations human readable. For example, instead of a user having a value for their country as 1, they want it to be "USA". Instead of a value of 14 for their state they way to see "CA" or "TX", etc.
My Migration Configuration.cs contains a seed routine to add countries and states.
When I run Update-Database, I get the following error:
Running Seed method.
System.Data.Entity.Infrastructure.DbUpdateException: Unable to
determine the principal end of the 'XXX_XXX_XXX.Models.State_Country'
relationship. Multiple added entities may have the same primary key.
---> System.Data.Entity.Core.UpdateException: Unable to determine the principal end of the 'XXXX_XXXX_XXXX.Models.State_Country'
relationship. Multiple added entities may have the same primary key.
My ViewModel contains:
namespace XXXX_XXXX_XXXX.Models
{
public class Country
{
[Key]
public string CountryCode { get; set; }
public int CountryId { get; set; }
public string CountryName { get; set; }
public virtual ICollection<State> States { get; set; }
}
public class State
{
[Key]
public string StateCode { get; set; }
public int StateId { get; set; }
public string StateName { get; set; }
[ForeignKey("Country")]
public string CountryCode { get; set; }
public virtual Country Country { get; set; }
}
}
My seed routine looks like this:
protected override void Seed(ProgramDbContext context)
{
// Initialize Countries
context.Countries.AddOrUpdate(c => c.CountryCode,
new Country { CountryId = 1,
CountryName = "United States",
CountryCode = "USA" });
context.Countries.AddOrUpdate(c => c.CountryCode,
new Country { CountryId = 2,
CountryName = "Canada",
CountryCode = "CAN" });
{ ... }
// Initialize States
context.States.AddOrUpdate(c => c.StateId,
new State { StateId = 1,
StateName = "Iowa",
StateCode = "IA",
CountryCode = "USA" });
context.States.AddOrUpdate(c => c.StateId,
new State { StateId = 2,
StateName = "Illinois",
StateCode = "IL",
CountryCode = "USA" });
context.States.AddOrUpdate(c => c.StateId,
new State { StateId = 3,
StateName = "California",
StateCode = "CA",
CountryCode = "USA" });
{ ... }
}
I DO understand that I am getting the error because CountryCode doesn't contain unique values for every column. If I set the CountryId to the Key and change the Foreign Key for States to CountryId I don't get the error, but then it seems I have to perform a lookup on the Id fields to get the CountryCode whenever I need it.
I also foresee that it's possible and even likely that the StateCode will eventually have a duplicate, though it doesn't currently.
Is it possible to have 2 [Key] attributes in a single table using ViewModel coding? I couldn't get that to work.
Do I need to place the [Key] attribute on the Id columns of each of these tables and then look-up the CountryCode and StateCode each time I need to update a user profile or create a dropdown list? Btw, I've created custom Identity fields for CountryCode and StateCode.
How should my ViewModel for Country and State be coded so that I can conveniently have my user profiles show "CA, USA" instead of 3, 1 - for example?
EDIT
#David offered to look at my View code for the Country and State dropdowns. I populate the State dropdown using jQuery
I populate Model.Countries in my controller with:
public IEnumerable<SelectListItem> GetCountryList()
{
var countries = new SelectList(dbLocations.Countries, "CountryCode", "CountryName").ToList();
countries.Insert(0, (new SelectListItem { Text = "Select Country", Value = "-1" }));
countries.Insert(1, (new SelectListItem { Text = "United Sates", Value = "54" }));
countries.Insert(2, (new SelectListItem { Text = "Canada", Value = "13" }));
countries.Insert(3, (new SelectListItem { Text = "Mexico", Value = "12" }));
countries.Insert(4, (new SelectListItem { Text = "Brazil", Value = "36" }));
countries.Insert(5, (new SelectListItem { Text = "------------------------", Value = "-1" }));
IEnumerable<SelectListItem> countrylist =
countries.Select(m => new SelectListItem()
{
Text = m.Text,
Value = m.Value
});
return countrylist;
}
An example of how I use in a View:
<div class="form-group">
#Html.LabelFor(m => m.CountryId, new { #class = "col-md-3 control-label" })
<div class="col-md-9">
#Html.DropDownListFor(
x => x.CountryId,
new SelectList(Model.Countries, "Value", "Text"),
new { #class = "form-control" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(m => m.StateId, new { #class = "col-md-3 control-label" })
<div class="col-md-9">
<div id="stateid_select">
<select id="StateId" class="form-control" name="StateId"></select>
</div>
</div>
</div>
jQuery for Country change (retrieves states):
<script type="text/javascript">
// Populate State/Province dropdown on Country select
$(function () {
$('#CountryCode').change(function () {
$.getJSON('/Account/StateList/' + $('#CountryCode').val(), function (data) {
var cnt = 0;
var items = '<option>Select a State/Province</option>';
$.each(data, function (i, state) {
items += "<option value='" + state.Value + "'>" + state.Text + "</option>";
cnt = cnt + 1;
});
// Hide SELECT and show TEXT field if no state/provinces to choose from.
// Else show SELECT and hide TEXT field.
if (!cnt == 0) {
$('#stateprovince_select select').html(items).show().attr("disabled", false).removeClass("hide");
$('#stateprovince_text input[type="text"]').hide().attr("disabled", true).addClass("hide");
} else {
$('#stateprovince_select select').html('').hide().attr("disabled", true).addClass("hide");
$('#stateprovince_text input[type="text"]').show().attr("disabled", true).removeClass("hide");
$('#StateProvinceCode').hide();
}
});
});
});
</script>
jQuery calls this routine in Controller:
[AllowAnonymous]
[AcceptVerbs(HttpVerbs.Get)]
public JsonResult StateList(int countryid)
{
var state = from s in dbLocations.States
where s.CountryId == countryid
select s;
return Json(new SelectList(state.ToArray(), "StateId", "StateName"), JsonRequestBehavior.AllowGet);
}
May be more than you needed to see. I made some changes to reflect the ModelView changes, though I haven't been able to run the app yet - still making changes. The jQuery needs some cleaning up, I know. ;)
You should leave the Key index on the Id column but add a unique constraint to the CountryCode. This has the following benefits:
You relationships are all using the integer Id columns which will perform better than joining on strings.
The CountryCode will now not allow duplicate entries.
The CountryCode can be shown to users and hides away the fact your database is linking with integers.
So now your models look like this:
public class Country
{
[Key]
public int Id { get; set; }
[Index("UX_Country_CountryCode", IsUnique = true)]
[MaxLength(10)]
public string CountryCode { get; set; }
public string CountryName { get; set; }
public virtual ICollection<State> States { get; set; }
}
public class State
{
[Key]
public int Id { get; set; }
[Index("UX_State_StateCode", IsUnique = true)]
[MaxLength(10)]
public string StateCode { get; set; }
public string StateName { get; set; }
[ForeignKey("Country")]
public int CountryId { get; set; }
public virtual Country Country { get; set; }
}
If you want to be able to have the same state code in different countries, extend the unique constraint like this:
public class State
{
[Key]
public int Id { get; set; }
[Index("UX_State_StateCodeCountryId", IsUnique = true, Order = 1)]
[MaxLength(10)]
public string StateCode { get; set; }
public string StateName { get; set; }
[ForeignKey("Country")]
[Index("UX_State_StateCodeCountryId", IsUnique = true, Order = 0)]
public int CountryId { get; set; }
public virtual Country Country { get; set; }
}

Trim string NHIBERNATE

Can't believe I've found no answer to this but how can you do a query like
SELECT LTRIM(RTRIM("ColumnName")) FROM ....
in NHibernate
thanks
Having an example of Bank as POCO:
public class Bank
{
public virtual int ID { get; set; }
public virtual string City { get; set; }
public virtual string Street { get; set; }
}
There is a syntax for the LTRIM(RTRIM...
Bank bank = null;
var session = ...;
var query = session.QueryOver<BankAddress>()
.SelectList(l => l
// properties ID, City
.Select(c => c.ID).WithAlias(() => bank.ID)
.Select(c => c.City).WithAlias(() => bank.City)
// projection Street
.Select(Projections.SqlProjection(
" LTRIM(RTRIM({alias}.Street)) as Street" // applying LTRIM(RTRIM
, new string[] { "Street" }
, new IType[] { NHibernate.NHibernateUtil.String }
))
.TransformUsing(Transformers.AliasToBean<Bank>())
;
var list = query.List<Bank>();