Inner join on many to many relation - inner-join

I have those 2 entities in my symfony project : house and software.
Many Home can have many software and Many software can belongs to many home
I'm trying to get only the Homes that have , let's say the software n° 1 + software n°2.
Actually I've managed to retrieve the Homes that have software n°1 and those that have software n°2 but not both those that have soft 1 + soft2
If I'm not wrong, It should be a Inner join join, right ?
Here's my entities and repository's method :
class Software {
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Home", mappedBy="softwares")
*/
private $homes;
public function __constuct() {
$this->homes = new ArrayCollection();
}
// ...
public function getHomes(){ ... }
public function addHome(Home $home){ ... }
// ...
}
class Home {
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Software", inversedBy="homes")
*/
private $softwares;
public function __constuct() {
$this->softwares = new ArrayCollection();
}
//...
public function getSoftwares(){ ... }
public function addSoftware(Software $software){ ... }
//...
}
Home repository
class HomeRepository extends ServiceEntityRepository {
public function innerJoinSoftware($softIds)
{
$qb = $this->createQueryBuilder('c')
->innerJoin('c.softwares', 's')
->andWhere('s.id IN(:softIds)')
->setParameter('softIds', $softIds)
;
return $qb->getQuery()->getResult();
}
}
To illustrate my point :
Home1 has soft1, soft2
Home2 has soft1, soft3
Home3 has soft2, soft3
What I wanna do is something like
dump(homeRepo->innerJoinSoftware([1, 2]));
//should output Home1 but actually I have
//it outputs Home1, Home2, Home3
Here's the SQL version I came out with, but I'm still not able to do it with Doctrine
SELECT home.id, home.name FROM Home as home
INNER JOIN (
SELECT home_id as home_id, COUNT(home_id) as count_home
FROM home_software
WHERE software_id IN (1, 2)
GROUP BY home_id
HAVING count_home = 2) as soft # count_home should be dynamic
ON home.id = soft.home_id
ORDER BY home.name

Here's how I solve this problem (helped by the raw SQL I've posted)
public function findBySoftwaresIn($softIds)
{
//retrieve nbr of soft per home
$nbrSoftToFind = count($softIds);
$qb = $this->createQueryBuilder('h');
$qb->innerJoin('h.softwares', 's')
->andWhere('h.id IN (:softIds)')
->setParameter('softIds', $softIds)
//looking for home coming back by nbrSoft
->andHaving($qb->expr()->eq($qb->expr()->count('h.id'), $nbrSoftToFind))
->groupBy('h.id')//dont forget to group by ID
->addOrderBy('h.name')
;
return $qb->getQuery()->getResult();
}

Related

Adding addCondition() in a Form with fields from other tables

I don't know the syntax to access "CurrentTable.ForeignKey" nor "OtherTable.PrimaryKey" in a $model->addCondition() statement.
This is a fragment of my code which works:
$mm = new SYSPC_MODEL($this->app->db,['title_field'=>'MODEL_NAME']);
$mm->addCondition('MODEL_NAME', 'LIKE', 'DESK%');
In place of simply searching for MODEL_NAME like 'DESK%', I would like to display only the FK_MODEL_id values which exist in the SYSPC_MODEL table for the same FK_OS_ID than the current record FK_OS_ID value. So in SQL, we should have something like:
SELECT SYSPC_MODEL.MODEL_NAME WHERE ( DHCP_PC.FK_OS_ID = SYSPC_MODEL.id )
To understand easier the context, I reduced my code as much as possible:
<?php
include_once ('../include/config.php');
require '../vendor/autoload.php';
class SYSPC_OS extends \atk4\data\Model {
public $table = 'SYSPC_OS';
function init() {
parent::init();
$this->addFields([ ['OS_NAME', 'required'=>true, 'caption'=>'Identifiant d\'OS'],
['OS_DESCRIPTION', 'required'=>true, 'caption'=>'Description d\'OS']
]);
}
} // End of class SYSPC_OS
class SYSPC_MODEL extends \atk4\data\Model {
public $table = 'SYSPC_MODEL';
function init() {
parent::init();
$this->addFields([ ['MODEL_NAME', 'caption'=>'Nom du modele'],
['MODEL_BASE_RPM', 'caption'=>'Rpm de base']
]);
$this->hasOne('FK_OS_id',[new SYSPC_OS(),'ui'=>['visible'=>false]])->addField('OS_NAME','OS_NAME');
}
} // End of class SYSPC_MODEL
class DHCP_PC extends \atk4\data\Model {
public $table = 'DHCP_PC';
function init() {
parent::init();
$this->addFields([ ['PCNAME', 'required'=>true, 'caption'=>'Nom du pc']
]);
$this->hasOne('FK_OS_ID',['required'=>true,new SYSPC_OS(),'ui'=>['visible'=>false]])->addField('OS_NAME','OS_NAME');
$this->setOrder('PCNAME','asc');
$this->hasOne('FK_MODEL_id',['required'=>true,new SYSPC_MODEL(),'ui'=>['visible'=>false]])->addField('MODEL_NAME','MODEL_NAME');
}
} // End of class DHCP_PC
class PcForm extends \atk4\ui\Form {
function setModel($m, $fields = null) {
$PcWidth = 'three';
parent::setModel($m, false);
$gr = $this->addGroup('PC name');
$gr->addField('PCNAME',['required'=>true,'caption'=>'Nom du pc']);
$gr = $this->addGroup('OS');
$mm2 = new SYSPC_OS($this->app->db,['title_field'=>'OS_NAME']);
$gr->addField('FK_OS_ID',['width'=>$PcWidth],['DropDown'])->setModel($mm2);
$gr = $this->addGroup('Modèle');
$mm = new SYSPC_MODEL($this->app->db,['title_field'=>'MODEL_NAME']);
$mm->addCondition('MODEL_NAME', 'LIKE', 'DESK%'); // Works fine but I would like to display only the FK_MODEL_id values
// which exist in the SYSPC_MODEL table for the same FK_OS_ID
// than the current record FK_OS_ID value :
// SELECT SYSPC_MODEL.MODEL_NAME WHERE ( DHCP_PC.FK_OS_ID = SYSPC_MODEL.id )
$gr->addField('FK_MODEL_id', ['width'=>$PcWidth], ['DropDown'])->setModel($mm);
return $this->model;
}
} // End of class PcForm
$app = new \atk4\ui\App();
$app->title = 'Gestion des PC';
$app->initLayout($app->stickyGET('layout') ?: 'Admin');
$app->db = new \atk4\data\Persistence_SQL(
"pgsql:host=".$GLOBALS['dbhost'].";dbname=".$GLOBALS['dbname'],
$GLOBALS['dbuser'],
$GLOBALS['dbpass']
);
$g = $app->add(['CRUD', 'formDefault'=>new PcForm()]);
$g->setIpp([10, 25, 50, 100]);
$g->setModel(new DHCP_PC($app->db),['PCNAME', 'OS_NAME', 'MODEL_NAME']);
?>
Please look at https://github.com/atk4/ui/pull/551 - it might be what you're looking for.
Example here: https://ui.agiletoolkit.org/demos/autocomplete.php
Docs: https://agile-ui.readthedocs.io/en/latest/autocomplete.html?highlight=lookup#lookup-field
$form = $app->add(new \atk4\ui\Form(['segment']));
$form->add(['Label', 'Add city', 'top attached'], 'AboveFields');
$l = $form->addField('city',['Lookup']);
// will restraint possible city value in droddown base on country and/or language.
$l->addFilter('country', 'Country');
$l->addFilter('language', 'Lang');
//make sure country and language belong to your model.
$l->setModel(new City($db));
Alternatively you can use something other than drop-down, here is UI example:
https://ui.agiletoolkit.org/demos/multitable.php
Selecting value in the first column narrows down options in the next. You can have a hidden field inside your form where you can put the final value.
Thanks for your support but I still have some questions.
Question 1: I found "addRelatedEntity" and "relEntity" but I didn't found a description of those commands. Does it exist ? Is this a possible solution for my issue ?
Question 2: Is it possible to 'Lookup' in another table and if yes, how ?
Question 3: If 'Lookup' is not the solution, how to make a join (with filtering in the where clause) inside a model ?
Question 4: If the join is not the solution, is it possible to use DSQL inside a model ?
Question 5: Or do you have a DSQL example (with a self made join between several tables) associated with a CRUD ?

NHibernate Convert query to async query

I'm looking at async-ifying some of our existing code. Unfortunately my experience with NHibernate is lacking. Most of the NHibernate stuff has been easy, considering NHibernate 5 has a lot of support for async. I am, however, stuck.
Originally, we do something like this using our Dependency Injection:
private readonly IRepository repository;
public MovieRepository(IRepository repository)
{
this.repository = repository;
}
public Movie Get(int id)
{
return (from movie in repository.Query<Movie>()
select new Movie
{
ID = movie.ID,
Title = movie.Title,
Genre = new Genre
{
ID = movie.Genre.ID,
Name = movie.Genre.Name,
},
MaleLead = movie.MaleLead,
FemaleLead = movie.FemaleLead,
}).FirstOrDefault();
}
//Repository Query method in Repository.cs
public IQueryable<TEntity> Query<TEntity>() where TEntity : OurEntity
{
session = session.OpenSession();
return from entity in session.Query<TEntity>() select entity;
}
This works great for our current uses. We write things this way to maintain control over our queries, especially related to more complex objects, ensuring we get back exactly what we need.
I've tried a few things, like making the Query method return a Task< List< TEntity>> and using the ToListAsync() method, however because I am returning it as that kind of list I cannot query on it.
I'm sure I've missed something. If anyone can help me out, I would appreciate it.
You need to use FirstOrDefaultAsync in this case.
public async Task<Movie> Get(int id)
{
return await (from movie in repository.Query<Movie>()
select new Movie
{
ID = movie.ID,
Title = movie.Title,
Genre = new Genre
{
ID = movie.Genre.ID,
Name = movie.Genre.Name,
},
MaleLead = movie.MaleLead,
FemaleLead = movie.FemaleLead,
}).FirstOrDefaultAsync();
}
Add this using statement to your file
using NHibernate.Linq;
Then you can change your method to
public async Task<Movie> Get(int id)
{
return await (from movie in repository.Query<Movie>()
select new Movie
{
ID = movie.ID,
Title = movie.Title,
Genre = new Genre
{
ID = movie.Genre.ID,
Name = movie.Genre.Name,
},
MaleLead = movie.MaleLead,
FemaleLead = movie.FemaleLead,
}).FirstOrDefaultAsync();
}
NB: This is only available from NHibernate 5
Addendum:
The code you have in Repository.cs can be simplified to something like this:
//Repository Query method in Repository.cs
public IQueryable<TEntity> Query<TEntity>() where TEntity : OurEntity
{
//session = session.OpenSession(); //this is obviously wrong, but it's beside the point
var session = sessionFactory.OpenSession();
return session.Query<TEntity>(); //the fix
}

Testing with Community User, getNetworkId return null

#isTest
public static void TestEmptySearchQuery() {
User thisUser = [ select Id from User where Id = :UserInfo.getUserId() ];
System.runAs ( thisUser ) { // running as thisUser to Avoid Error: MIXED_DML_OPERATION
setupData(); // inside setupData, community is created successfully
generateUser(); // List of user assigned with some profile, as required for project.
list<PermissionSetAssignment> PSA = new list<PermissionSetAssignment> ();
PermissionSet ps = [SELECT Id, name FROM PermissionSet where name='Some_Access'];
system.debug('PermissionSet ' + ps);
for(user u:userList)
PSA.add(new PermissionSetAssignment(AssigneeId = u.id, PermissionSetId = ps.Id)); // all the user assgined with some_access based on requirement of project
insert PSA;
}
Test.startTest();
User usr = [select Id from User where Id = :userList[0].id];
System.runAs(usr) {
system.debug('Network ommunityId ****' + Network.getNetworkId()); //getting null
SomeClass obj = new SomeClass();
Id Nid=obj.fetchNetworkId(); // return null;
system.debug('network id ' + Nid); // null
}
Test.stopTest();
}
class SomeClass {
//some code
public id fetchNetworkId() {
system.debug('network id ' + Network.getNetworkId()); // network id null;
return Network.getNetworkId(); // return null
}
// some code
}
While running normally page, controller returns proper network id,
when try to write a test class for this, community network id always return null.
the user you use for the runAs needs to be part of a community, so you need to basically create an account, create a contact and then a user for that contact. That makes the runAS user to be part of a community

Sorting information from related tables - Symfony2

A. I have a table with users and a table with friends and here are their connections:
This in the User class:
/**
* #ORM\OneToMany(targetEntity="Friend", mappedBy="user")
*/
protected $friends;
public function __construct()
{
$this->friends = new ArrayCollection();
}
and this is in the Friend class:
/**
* #ORM\ManyToOne(targetEntity="User", inversedBy="friends")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
protected $user;
I have an template, in which I render the friends of the current user just using
$friends = $user->getFriends();
But now I want to be able to sort the friends of the user. I created a repository class with this function:
public function findAllOrderedByName()
{
return $this->getEntityManager()
->createQuery('SELECT f FROM EMMyFriendsBundle:Friend f ORDER BY f.name ASC')
->getResult();
}
but it displays all the friends of all the users, not just the current user. Is there a way to show only the friends of the current user without using something like "WHERE friend.user_id = user.id"? If yes, please tell me how to do it.
B. Another thing I would like to ask is how to manage my controllers.
This is the action which renders the template to show all unsorted friends:
/*
* #Displays the basic information about all the friends of an user
*/
public function displayAction()
{
$user = $this->get('security.context')->getToken()->getUser();
if($user->getName() === NULL)
{
$name = $user->getUsername();
}
else
{
$name = $user->getName();
}
$this->get('session')->setFlash('hello', 'Hello, '.$name.'!');
$friends = $user->getFriends();
$cat = new Category();
$dd_form = $this->createForm(new \EM\MyFriendsBundle\Entity\ChooseCatType(), $cat);
return $this->render('EMMyFriendsBundle:Home:home.html.twig', array(
'name' => $name, 'friends' => $friends, 'user' => $user,
'dd_form' => $dd_form->createView()));
}
And I have a sortAction, but I don't know how to write it. It should be the same as the above, only the row
$friends = $user->getFriends();
should be replaced with
$friends = $em->getRepository('EMMyFriendsBundle:Friend')
->findAllOrderedByName();
but if I copy it, it's a lot code duplication and it's stupid. I thought of one controller with an if-statement, but what to have in it? Please give me some ideas! Thanks in advance!
Do this:
/**
* #ORM\OneToMany(targetEntity="Friend", mappedBy="user")
* #ORM\OrderBy({"name" = "ASC"})
*/
protected $friends;

Where to define security roles?

I have an User and a Group Entity which both hold an array of roles.
Now I would like to keep the option open to modificate the roles, add them and so on.
Should I use constants in the classes for this or should I relate an OneToOne-relation to a table which keeps all the roles?
Best Regards,
pus.dev
User <=> Role
Group <=> Role
public function getRoles()
{
$roles = $this->roles;
foreach ($this->getGroups() as $group) {
$roles = array_merge($roles, $group->getRoles());
}
// we need to make sure to have at least one role
$roles[] = static::ROLE_DEFAULT;
return array_unique($roles);
}
How about creating a Roles table with ManyToOne relation to each user. One row of the Roles table would contain a role(string or constant int) and a user.
Alternatively you can create a Roles table and have a ManyToMany relation with the User table. Using this you could define roles dynamically, so you don't have to hardcode the possible roles.
In the OneToMany case you could retrieve the roles by writing such a function:
/** #OneToMany(...) */
/** $roles contains strings */
protected $roles;
public function getRoles() {
return $this->roles;
}
OR
/** #OneToMany(...) */
/** $roles contains integers */
protected $roles;
public function getRoles() {
$rolesArr = array(1 => 'ROLE_ADMIN', 2 => 'ROLE_USER', 3 => 'ROLE_EDITOR'); // you should refactor $rolesArr
$retRoles = array();
foreach($this->roles as $role) {
$retRoles[] = $rolesArr[$role];
}
return $retRoles;
}
In the ManyToMany case you could retrieve the roles by writing such a function:
/** #ManyToMany(...) */
protected $roles;
// ...
public function getRoles() {
$retRoles = array();
// symfony2 requires a string array
foreach($this->roles as $role) {
$retRoles[] = $role->getName(); // or $retRoles[] = 'ROLE_' . $role->getName();
}
return $retRoles;
}
And dont forget that your User model must implement symfony's built-in User interface.
For group roles you can do this:
class Group
{
/** #ManyToMany(...) */
protected $roles;
public function getRoles() {
return $this->roles;
}
}
class User
{
/** #ORM\Column(...) */
protected $group;
/** #ManyToMany(...) */
protected $roles;
// ...
public function getRoles() {
$retRoles = array();
// symfony2 requires a string array
$roles = $this->roles->merge($this->group->getRoles());
foreach($roles as $role) {
$retRoles[] = $role->getName(); // or $retRoles[] = 'ROLE_' . $role->getName();
}
return $retRoles;
}
}

Resources