Related
I created a CRUD which manages events, another other CRUD which manages styles of music.
I made a manytomany relationship between the two, and I want to choose from my event creation form my style of music.
So I made a formtype for the events that I use for the creation and for the edition.
The crud worked very well with me doing my relationship. The creation of event still works but I have this error when I want to access the view of the modification form of my event :
Unable to transform value for property path "music_type": Expected a Doctrine\Common\Collections\Collection object.
what do you think ?
part music style for my eventtype :
->add('music_type', EntityType::class, [
'class' => MusicStyle::class,
'query_builder' => function (MusicStyleRepository $r) {
return $r->createQueryBuilder('i')
->orderBy('i.name', 'ASC');
},
'label' => 'Style de musique',
'label_attr' => [
'class' => 'form-label mt-4'
],
'choice_label' => 'name',
'multiple' => true,
'expanded' => true,
])
My entity Event :
* #return Collection<int, MusicStyle>
*/
public function getMusicStyles(): Collection
{
return $this->musicStyles;
}
public function addMusicStyle(MusicStyle $musicStyle): self
{
if (!$this->musicStyles->contains($musicStyle)) {
$this->musicStyles[] = $musicStyle;
$musicStyle->addEvent($this);
}
return $this;
}
public function removeMusicStyle(MusicStyle $musicStyle): self
{
if ($this->musicStyles->removeElement($musicStyle)) {
$musicStyle->removeEvent($this);
}
return $this;
}
My eventController
public function edit(
EventRepository $repository,
Event $event,
Request $request,
EntityManagerInterface $manager
): Response {
$form = $this->createForm(EventType::class, $event);
$form->handleRequest($request);
if($form->isSubmitted() && $form->isValid()) {
$event = $form->getData();
$manager->persist($event);
$manager->flush();
$this->addFlash(
'success',
'Ton event a bien été modifié !'
);
return $this->redirectToRoute('event.index');
}
return $this->render('pages/event/edit.html.twig', [
'form' => $form->createView()
]);
}
my migration for the event table
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE event (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, description LONGTEXT NOT NULL, picture VARCHAR(255) NOT NULL, city VARCHAR(255) NOT NULL, adress VARCHAR(255) NOT NULL, place_name VARCHAR(255) DEFAULT NULL, date DATETIME NOT NULL, music_type VARCHAR(255) NOT NULL, event_type VARCHAR(255) NOT NULL, participants INT DEFAULT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
$this->addSql('CREATE TABLE messenger_messages (id BIGINT AUTO_INCREMENT NOT NULL, body LONGTEXT NOT NULL, headers LONGTEXT NOT NULL, queue_name VARCHAR(190) NOT NULL, created_at DATETIME NOT NULL, available_at DATETIME NOT NULL, delivered_at DATETIME DEFAULT NULL, INDEX IDX_75EA56E0FB7336F0 (queue_name), INDEX IDX_75EA56E0E3BD61CE (available_at), INDEX IDX_75EA56E016BA31DB (delivered_at), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
}
my migration for the ManyToMany
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE music_style (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(50) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
$this->addSql('CREATE TABLE music_style_event (music_style_id INT NOT NULL, event_id INT NOT NULL, INDEX IDX_5F0B2D4F7DDE3C52 (music_style_id), INDEX IDX_5F0B2D4F71F7E88B (event_id), PRIMARY KEY(music_style_id, event_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
$this->addSql('ALTER TABLE music_style_event ADD CONSTRAINT FK_5F0B2D4F7DDE3C52 FOREIGN KEY (music_style_id) REFERENCES music_style (id) ON DELETE CASCADE');
$this->addSql('ALTER TABLE music_style_event ADD CONSTRAINT FK_5F0B2D4F71F7E88B FOREIGN KEY (event_id) REFERENCES event (id) ON DELETE CASCADE');
}
this may seem like a silly question so i'm novice in symfony
Try changing the property type from Collection to ArrayCollection instead. Because the querybuilder returns an array, not a Collection.
Another thing to check is, if you are initializing the property in your entity constructor, like
public function __construct()
{
$this->musicStyles = new ArrayCollection();
}
I can insert table column names dynamically the problem is when I want to insert the new or old values in my log table I am getting a string 'old.Colname' or 'new.Colname' instead of old or new value.
DECLARE C_USER VARCHAR(255);
DECLARE VARIABLE OPERATION_EVENT CHAR(8);
DECLARE GROUPIDNO INT;
DECLARE LOGDATAID_NO INT;
DECLARE VARIABLE FN CHAR(31);
DECLARE VARIABLE NEWCOL CHAR(31);
DECLARE VARIABLE OLDCOL CHAR(31);
BEGIN
SELECT CURRENT_USER FROM RDB$DATABASE INTO :C_USER;
IF (DELETING) THEN
BEGIN
OPERATION_EVENT = 'DELETE';
END
ELSE
BEGIN
IF (UPDATING) THEN
OPERATION_EVENT = 'UPDATE';
ELSE
OPERATION_EVENT = 'INSERT';
END
SELECT MAX(GROUPID) FROM LOG_DATA INTO :GROUPIDNO;
IF(GROUPIDNO IS NULL) THEN
GROUPIDNO = 1;
ELSE
GROUPIDNO = GROUPIDNO + 1;
IF(INSERTING) THEN
BEGIN
FOR SELECT RDB$FIELD_NAME FROM RDB$RELATION_FIELDS WHERE RDB$RELATION_NAME = 'ARAC' INTO :FN DO
BEGIN
OLDCOL = 'old.'||:FN;
NEWCOL = 'new.'||:FN;
INSERT INTO LOG_DATA (OLD_VALUE,NEW_VALUE, COLUMN_NAME, TABLE_NAME, OPERATION,
CREATEDAT,USERS,GROUPID,LOGDATAID)
VALUES (:OLDCOL,:NEWCOL,:FN,'ARAC',trim(:OPERATION_EVENT),
current_timestamp,:C_USER,:GROUPIDNO,:LOGDATAID_NO + 1);
END
END
Here is a screen shot of my log table, I want to insert the old and new values, but column names are being inserted as strings instead
The problem is that you are trying to reference the old and new context as strings, and that is not possible. The specific problem is:
OLDCOL = 'old.'||:FN;
NEWCOL = 'new.'||:FN;
This produces a string with value 'old.<whatever the value of FN is>' (and same for new). It does not produce the value of the column with the name in FN from the OLD or NEW contexts.
Unfortunately, it is not possible to dynamically reference the columns in the OLD and NEW contexts by name. You will explicitly need to use OLD.columnname and NEW.columnname in your code, which means that you will need to write (or generate) a trigger that inserts each column individually.
Alternatively, you could upgrade to Firebird 3, and use a UDR to define a trigger in native code, C# or Java (or other supported languages). These UDR engines allow you to dynamically reference the columns in the old and new context.
As an example, using the FB/Java external engine (check the readme in the repository on how to install FB/Java):
Create a CHANGELOG table:
create table changelog (
id bigint generated by default as identity constraint pk_changelog primary key,
tablename varchar(31) character set unicode_fss not null,
row_id varchar(30) character set utf8,
columnname varchar(31) character set unicode_fss not null,
new_value varchar(2000) character set utf8,
old_value varchar(2000) character set utf8,
operation char(6) character set ascii not null,
modification_datetime timestamp default localtimestamp not null
)
And a FB/Java trigger:
package nl.lawinegevaar.fbjava.experiment;
import org.firebirdsql.fbjava.TriggerContext;
import org.firebirdsql.fbjava.Values;
import org.firebirdsql.fbjava.ValuesMetadata;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiPredicate;
import static java.util.Collections.unmodifiableSet;
import static org.firebirdsql.fbjava.TriggerContext.Action.*;
public class ChangelogTrigger {
private static final Set<TriggerContext.Action> SUPPORTED_ACTIONS =
unmodifiableSet(EnumSet.of(INSERT, UPDATE, DELETE));
private static final int ROW_ID_LENGTH = 30;
private static final int VALUE_LENGTH = 2000;
private static final String INSERT_CHANGELOG =
"insert into changelog (tablename, row_id, columnname, new_value, old_value, operation) "
+ "values (?, ?, ?, ?, ?, ?)";
public static void logChanges() throws SQLException {
TriggerContext ctx = TriggerContext.get();
TriggerContext.Action action = ctx.getAction();
if (!SUPPORTED_ACTIONS.contains(action)) {
return;
}
String tableName = ctx.getTableName();
if (tableName.equals("CHANGELOG")) {
throw new IllegalStateException("Cannot apply ChangelogTrigger to table " + tableName);
}
String identifierColumn = ctx.getNameInfo();
ValuesMetadata fieldsMetadata = ctx.getFieldsMetadata();
int identifierIdx = identifierColumn != null ? fieldsMetadata.getIndex(identifierColumn) : -1;
Values oldValues = ctx.getOldValues();
Values newValues = ctx.getNewValues();
Values primaryValueSet = action == INSERT ? newValues : oldValues;
String identifierValue = identifierIdx != -1
? truncate(asString(primaryValueSet.getObject(identifierIdx)), ROW_ID_LENGTH)
: null;
try (PreparedStatement pstmt = ctx.getConnection().prepareStatement(INSERT_CHANGELOG)) {
pstmt.setString(1, tableName);
pstmt.setString(2, identifierValue);
BiPredicate<Object, Object> logColumn = action == UPDATE
? ChangelogTrigger::acceptIfModified
: ChangelogTrigger::acceptAlways;
boolean batchUsed = false;
for (int idx = 1; idx <= fieldsMetadata.getParameterCount(); idx++) {
Object oldValue = oldValues != null ? oldValues.getObject(idx) : null;
Object newValue = newValues != null ? newValues.getObject(idx) : null;
if (logColumn.test(oldValue, newValue)) {
String columnName = fieldsMetadata.getName(idx);
pstmt.setString(3, columnName);
pstmt.setString(4, truncate(asString(newValue), VALUE_LENGTH));
pstmt.setString(5, truncate(asString(oldValue), VALUE_LENGTH));
pstmt.setString(6, action.name());
pstmt.addBatch();
batchUsed = true;
}
}
if (batchUsed) {
pstmt.executeBatch();
}
}
}
private static boolean acceptAlways(Object oldValue, Object newValue) {
return true;
}
private static boolean acceptIfModified(Object oldValue, Object newValue) {
return !Objects.equals(oldValue, newValue);
}
private static String asString(Object value) {
return value != null ? String.valueOf(value) : null;
}
private static String truncate(String value, int maxLength) {
if (value == null || value.length() <= maxLength) {
return value;
}
return value.substring(0, maxLength - 3) + "...";
}
}
This FB/Java trigger is very generic and can be used for multiple tables. I haven't tested this trigger with all datatypes. For example, to be able to make the trigger work correctly with columns of type blob or other binary types will require additional work.
Build the trigger and load it into the database using the fbjava-deployer utility of FB/Java.
Then define the trigger on the table you want (in this case, I defined it on the TEST_CHANGELOG table):
create trigger log_test_changelog
before insert or update or delete
on test_changelog
external name 'nl.lawinegevaar.fbjava.experiment.ChangelogTrigger.logChanges()!ID'
engine JAVA
The external name defines the routine to call (nl.lawinegevaar.fbjava.experiment.ChangelogTrigger.logChanges()) and the name of the (single) primary key column of the table (ID), which is used to log the identifier in the ROW_ID column.
I had a workable API backend design which can retrieve a user's all appointments, which are shown below. Now, instead of retrieve all, I'm planning to retrieve a user's appointments within future 30 days (from current time) from database, but I have no idea how to set limit to achieve this. Can anyone please help me with this?
Database.java
public List<Appointment> listUserAppointments(
String uid,
#Nullable String since,
#Nullable String until,
#Nullable AppointmentStatus status){
try {
AppointmentMapper mapper = session.getMapper(AppointmentMapper.class);
return mapper.getAppointmentsByUserId(uid, since, until, status);
} finally {
if (!keepAlive) close();
}
}
public Appointment getAppointment(String appointment_id) {
try {
AppointmentMapper mapper = session.getMapper(AppointmentMapper.class);
return mapper.getAppointmentById(appointment_id);
} finally {
if (!keepAlive) close();
}
}
AppointmentMapper.java
public interface AppointmentMapper {
/*
You can pass multiple parameters to a mapper method.
If you do, they will be named by the literal "param" followed
by their position in the parameter list by default,
for example: #{param1}, #{param2} etc.
If you wish to change the name of the parameters (multiple only),
then you can use the #Param("paramName") annotation on the parameter.
*/
Appointment getAppointmentById(String id);
List<Appointment> getAppointmentsByUserId(
#Param("uid") String uid,
#Param("since") String since,
#Param("until") String until,
#Param("status") AppointmentStatus status);
void updateUserNoteById(Appointment appointment);
void updateStatusById(Appointment appointment);
void insertAppointment(Appointment appointment);
void updateAppointment(Appointment appointment);
}
AppointmentAPI.java
#GET
#Path("me/appointments")
#Secured(UserRole.PATIENT)
#JSONP(queryParam = "callback")
#Produces(MediaType.APPLICATION_JSON)
public Response listMyAppointments(
#Context SecurityContext sc,
#QueryParam("since") String since,
#QueryParam("until") String until,
#QueryParam("is_confirmed") Boolean is_confirmed) {
String uid = sc.getUserPrincipal().getName();
List<Appointment> results = retrieveUserAppointments(uid, since, until, is_confirmed);
return Response.ok(results).build();
}
AppointmentMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.sec.lib.AppointmentMapper">
<select id="getAppointmentById" parameterType="String" resultType="com.sec.entity.Appointment">
SELECT * FROM Appointment WHERE id= #{id}
</select>
<select id="getAppointmentsByUserId" resultType="com.sec.entity.Appointment">
SELECT *
FROM Appointment
WHERE uid= #{uid}
<choose>
<when test="since != null and until != null">
AND date BETWEEN #{since} AND #{until}
</when>
<when test="since != null and until == null">
AND date > #{since}
</when>
<when test="since == null and until != null">
<![CDATA[
AND date < #{until}
]]>
</when>
</choose>
<choose>
<when test="status == null">
AND status != 'CANCELLED'
</when>
<otherwise>
AND status = #{status}
</otherwise>
</choose>
</select>
<update id="updateUserNoteById" parameterType="com.sec.entity.Appointment">
UPDATE Appointment SET
user_note= #{user_note}
WHERE id= #{id}
</update>
<update id="updateStatusById" parameterType="com.sec.entity.Appointment">
UPDATE Appointment SET
status= #{status}
WHERE id= #{id}
</update>
<insert id="insertAppointment" parameterType="com.sec.entity.Appointment">
INSERT INTO Appointment (id, uid, did, title, detail, date_create, date_change, date, duration, note, status)
VALUES (#{id}, #{uid}, #{did}, #{title}, #{detail}, #{date_create}, #{date_change}, #{date}, #{duration}, #{note}, #{status});
</insert>
<update id="updateAppointment" parameterType="com.sec.entity.Appointment">
UPDATE Appointment SET
did= #{did},
title= #{title},
detail= #{detail},
date_create= #{date_create},
date_change= #{date_change},
date= #{date},
duration= #{duration},
note= #{note},
status= #{status}
WHERE id= #{id}
</update>
</mapper>
Thank you for help :)
I tried to get last id, and read many post about it, but i don't arrive to apply it in my case.
First Class
private Date date;
private List<AdsEntity> adsDetails;
... getters and setters
Second Class (AdsEntity)
private int id;
private String description;
There is the code where i try to get the last id :
Mapper
#Insert({
"<script>",
"INSERT INTO tb_ads_details (idMyInfo, adDate)"
+ " VALUES"
+ " <foreach item='adsDetails' index='index' collection='adsDetails' separator=',' statement='SELECT LAST_INSERT_ID()' keyProperty='id' order='AFTER' resultType='java.lang.Integer'>"
+ " (#{adsDetails.description, jdbcType=INTEGER}) "
+ " </foreach> ",
"</script>"})
void saveAdsDetails(#Param("adsDetails") List<AdsDetailsEntity> adsDetails);
In debugging mode, when I watch List I see the id still at 0 and don't get any id.
So what I wrote didn't workout :(
Solution Tried with the answer from #Roman Konoval :
#Roman Konoval
I apply what you said, and the table is fully well set :)
Just one problem still, the ID is not fulfill
#Insert("INSERT INTO tb_ads_details SET `idMyInfo` = #{adsDetail.idMyInfo, jdbcType=INTEGER}, `adDate` = #{adsDetail.adDate, jdbcType=DATE}")
#SelectKey(statement = "SELECT LAST_INSERT_ID()", before = false, keyColumn = "id", keyProperty = "id", resultType = Integer.class )
void saveAdsDetails(#Param("adsDetail") AdsDetailsEntity adsDetail);
default void saveManyAdsDetails(#Param("adsDetails") List<AdsDetailsEntity> adsDetails)
{
for(AdsDetailsEntity adsDetail:adsDetails) {
saveAdsDetails(adsDetail);
}
}
Thank for your help :)
Solution add to #Roman Konoval proposal from #Chris advice
#Chris and #Roman Konoval
#Insert("INSERT INTO tb_ads_details SET `idMyInfo` = #{adsDetail.idMyInfo, jdbcType=INTEGER}, `adDate` = #{adsDetail.adDate, jdbcType=DATE}")
#SelectKey(statement = "SELECT LAST_INSERT_ID()", before = false, keyColumn = "id", keyProperty = "adsDetail.id", resultType = int.class )
void saveAdsDetails(#Param("adsDetail") AdsDetailsEntity adsDetail);
default void saveManyAdsDetails(#Param("adsDetails") List<AdsDetailsEntity> adsDetails)
{
for(AdsDetailsEntity adsDetail:adsDetails) {
saveAdsDetails(adsDetail);
}
}
Thanks to all of you, for the 3 suggestions!!!
yes. it doesnt work.
please take a look at mapper.dtd
foreach-tag doesnt support/provide the following properties statement, keyProperty order and resultType
if you need the id for each inserted item please let your DataAccessObject handle iteration and use something like this in your MapperInterface
#Insert("INSERT INTO tb_ads_details (idMyInfo, adDate) (#{adsDetail.idMyInfo, jdbcType=INTEGER}, #{adsDetail.adDate, jdbcType=DATE})")
#SelectKey(before = false, keyColumn = "ID", keyProperty = "id", resultType = Integer.class, statement = { "SELECT LAST_INSERT_ID()" } )
void saveAdsDetails(#Param("adsDetail") AdsDetailsEntity adsDetail);
please ensure AdsDetailsEntity-Class provides the properties idMyInfoand adDate
Edit 2019-08-21 07:25
some explanation
referring to the mentioned dtd the <selectKey>-tag is only allowed as direct child of <insert> and <update>. it refers to a single Object that is passed into the mapper-method and declared as parameterType.
its only executed once and its order property tells myBatis wether to execute it before or after the insert/update statement.
in your case, the <script> creates one single statement that is send to and handled by the database.
it is allowed to combine #Insert with <script> and <foreach> inside and #SelectKey. but myBatis doesnt intercept/observe/watch database handling the given statement. and as mentioned before, #SelectKey gets executed only once, before or after #Insert-execution. so in your particular case #SelectKey returns the id of the very last inserted element. if your script inserts ten elements, only the new generated id of tenth element will be returned. but #SelectKey requires a class-property with getter and setter to put the selected id into - which List<?> doesnt provide.
example
lets say you want to save an Advertisement and its AdvertisementDetails
Advertisement has an id, a date and details
public class Advertisement {
private List<AdvertisementDetail> adDetails;
private Date date;
private int id;
public Advertisement() {
super();
}
// getters and setters
}
AdvertisementDetail has its own id, a description and an id the Advertisementit belongs to
public class AdvertisementDetail {
private String description;
private int id;
private int idAdvertisement;
public AdvertisementDetail() {
super();
}
// getters and setters
}
the MyBatis-mapper could look like this. #Param is not used, so the properties are accessed direct.
#Mapper
public interface AdvertisementMapper {
#Insert("INSERT INTO tb_ads (date) (#{date, jdbcType=DATE})")
#SelectKey(
before = false,
keyColumn = "ID",
keyProperty = "id",
resultType = Integer.class,
statement = { "SELECT LAST_INSERT_ID()" })
void insertAdvertisement(
Advertisement ad);
#Insert("INSERT INTO tb_ads_details (idAdvertisement, description) (#{idAdvertisement, jdbcType=INTEGER}, #{description, jdbcType=VARCHAR})")
#SelectKey(
before = false,
keyColumn = "ID",
keyProperty = "id",
resultType = Integer.class,
statement = { "SELECT LAST_INSERT_ID()" })
void insertAdvertisementDetail(
AdvertisementDetail adDetail);
}
the DataAccessObject (DAO) could look like this
#Component
public class DAOAdvertisement {
#Autowired
private SqlSessionFactory sqlSessionFactory;
public DAOAdvertisement() {
super();
}
public void save(
final Advertisement advertisement) {
try (SqlSession session = this.sqlSessionFactory.openSession(false)) {
final AdvertisementMapper mapper = session.getMapper(AdvertisementMapper.class);
// insert the advertisement (if you have to)
// its new generated id is received via #SelectKey
mapper.insertAdvertisement(advertisement);
for (final AdvertisementDetail adDetail : advertisement.getAdDetails()) {
// set new generated advertisement-id
adDetail.setIdAdvertisement(advertisement.getId());
// insert adDetail
// its new generated id is received via #SelectKey
mapper.insertAdvertisementDetail(adDetail);
}
session.commit();
} catch (final PersistenceException e) {
e.printStackTrace();
}
}
}
What Chris wrote about inability to get ids in the foreach is correct. However there is a way to implement id fetching in mapper without the need to do it externally. This may be helpful if you use say spring and don't have a separate DAO layer and your mybatis mappers are the Repository.
You can use default interface method (see another tutorial about them) to insert the list of items by invoking a mapper method for single item insert and single item insert method does the id selection itself:
interface ItemMapper {
#Insert({"insert into myitem (item_column1, item_column2, ...)"})
#SelectKey(before = false, keyColumn = "ID",
keyProperty = "id", resultType = Integer.class,
statement = { "SELECT LAST_INSERT_ID()" } )
void saveItem(#Param("item") Item item);
default void saveItems(#Param("items") List<Item> items) {
for(Item item:items) {
saveItem(item);
}
}
MyBatis can assign generated keys to the list parameter if your DB/driver supports multiple generated keys via java.sql.Statement#getGeneratedKeys() (MS SQL Server, for example, does not support it, ATM).
The following example is tested with MySQL 5.7.27 + Connector/J 8.0.17 (you should include version info in the question).
Be sure to use the latest version of MyBatis (=3.5.2) as there have been several spec changes and bug fixes recently.
Table definition:
CREATE TABLE le tb_ads_details (
id INT PRIMARY KEY AUTO_INCREMENT,
description VARCHAR(32)
)
POJO:
private class AdsDetailsEntity {
private int id;
private String description;
// getters/setters
}
Mapper method:
#Insert({
"<script>",
"INSERT INTO tb_ads_details (description) VALUES",
"<foreach item='detail' collection='adsDetails' separator=','>",
" (#{detail.description})",
"</foreach>",
"</script>"
})
#Options(useGeneratedKeys = true, keyProperty="adsDetails.id", keyColumn="id")
void saveAdsDetails(#Param("adsDetails") List<AdsDetailsEntity> adsDetails);
Note: You should use batch insert (with ExecutorType.BATCH) instead of multi-row insert (=<foreach/>) when inserting a lot of rows.
I have a super simple table test e.g.
create table test (
id serial primary key,
status varchar (10)
);
insert into test (status)
values ('ready'), ('ready'),
('steady'),
('go'), ('go'), ('go'),
('new');
To get the aggregated counts I can run: -
1) Simple multi-row result using group by
select status,
count(id) as count
from test
group by status
... which returns ...
-------+-------
status | counts
-------+-------
go | 3
ready | 2
new | 1
steady | 1
-------+-------
2) Single-Row Result using jsonb_object_agg
with stats as (
select status,
count(id) as count
from test
group by status
)
select jsonb_object_agg (status, count) as status_counts from stats
... which returns ...
--------------------------------------------------
status_counts
--------------------------------------------------
{ "go" : 3, "new" : 1, "ready" : 2, "steady" : 1 }
--------------------------------------------------
Mybatis Interface method.
In my Java code (via MyBatis) I have a method: -
public Map<String, Integer> selectStatusCounts();
What I'm keen to figure out is how to map either query to the Map<String, Integer> Java object via MyBatis?
Update (1)
On a_horse_with_no_name advice and this stackover article I've come up with this: -
3) Single-Row Result using hstore
select hstore(array_agg(hs_key), array_agg(hs_value::text))
from (
select
status,
count(id) as count
from test
group by status
) x(hs_key,hs_value)
... which returns ...
--------------------------------------------------
status_counts
--------------------------------------------------
"go"=>"3", "new"=>"1", "ready"=>"2", "steady"=>"1"
--------------------------------------------------
Using something like this could maybe work: -
https://github.com/gbif/checklistbank/blob/master/checklistbank-mybatis-service/src/main/java/org/gbif/checklistbank/service/mybatis/postgres/HstoreCountTypeHandler.java
Will test now! :-)
Update (2)
Thanks a_horse_with_no_name again for your contributions - I'm very close now but still weirdness with MyBatis. Here is a type handler I created (so I can reuse the aggregations elsewhere): -
#MappedTypes(LinkedHashMap.class)
#MappedJdbcTypes(JdbcType.OTHER)
public class MyBatisMapHstoreToStringIntegerMap implements TypeHandler<Map<String, Integer>> {
public MyBatisMapHstoreToStringIntegerMap() {}
public void setParameter(PreparedStatement ps, int i, Map<String, Integer> map, JdbcType jdbcType) throws SQLException {
ps.setString(i, HStoreConverter.toString(map));
}
public Map<String, Integer> getResult(ResultSet rs, String columnName) throws SQLException {
return readMap(rs.getString(columnName));
}
public Map<String, Integer> getResult(ResultSet rs, int columnIndex) throws SQLException {
return readMap(rs.getString(columnIndex));
}
public Map<String, Integer> getResult(CallableStatement cs, int columnIndex) throws SQLException {
return readMap(cs.getString(columnIndex));
}
private Map<String, Integer> readMap(String hstring) throws SQLException {
if (hstring != null) {
Map<String, Integer> map = new LinkedHashMap<String, Integer>();
Map<String, String> rawMap = HStoreConverter.fromString(hstring);
for (Map.Entry<String, String> entry : rawMap.entrySet()) {
map.put(entry.getKey(), Integer.parseInt(entry.getValue())); // convert from <String, String> to <String,Integer>
}
return map;
}
return null;
}
}
... and here's the mapper interface ...
public interface TestMapper {
public Map<String, Integer> selectStatusCounts();
}
... and here is the <select> inside the XML mapper file ...
<select id="selectStatusCounts" resultType="java.util.LinkedHashMap">
select hstore(array_agg(hs_key), array_agg(hs_value::text)) as status_counts
from (
select
status,
count(id) as count
from test
group by status
) x(hs_key,hs_value)
</select>
However, it returns a Map with one entry called status_counts the value of which is the actual map I want i.e. {status_counts={new=1, ready=2, go=3, steady=1}}
The following is my maven dependencies with regards PostgreSQL / MyBatis: -
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.3.1</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>9.4-1201-jdbc41</version>
</dependency>
The easiest way is to define a hstore_agg() function:
CREATE AGGREGATE hstore_agg(hstore)
(
SFUNC = hs_concat(hstore, hstore),
STYPE = hstore
);
Then you can do this:
select hstore_agg(hstore(status, cnt::text))
from (
select status, count(*) cnt
from test
group by status
) t;
With the current JDBC driver Statement.getObject() will return a Map<String, String>.
As hstore only stores strings, it can't return a Map<String, Integer>
Posting an answer (which isn't perfect) but keen to see if anyone else has a better solution.
My answer is based on this stackoverflow answer: -
Return HashMap in mybatis and use it as ModelAttribute in spring MVC
I've created a POJO class called KeyValue: -
public class KeyValue<K, V> {
private K key;
private V value;
public KeyValue() {
}
public KeyValue(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() {
return key;
}
public void setKey(K key) {
this.key = key;
}
public V getValue() {
return value;
}
public void setValue(V value) {
this.value = value;
}
}
... and changed the test mapper method to ...
#MapKey("key")
public Map<String, KeyValue<String, Integer>> selectStatusCounts();
Note use of #MapKey parameter
I'm using the "1) Simple multi-row result using group by" SQL from original question and changed the result columns to key + value (so it maps to the new KeyValue object) as follows: -
<select id="selectStatusCounts" resultType="test.KeyValue">
select status as key,
count(id) as value
from bulk_upload
group by status
</select>
Accessing this in Java is achieved as follows: -
Map<String, KeyValue<String, Integer>> statusCounts = mapper.selectStatusCounts();
And to retrieve e.g. the value from map of new items we simply do: -
int numberOfstatusCounts = statusCounts.get("new").getValue();
I'm fairly happy with this solution but I would still prefer a Map<String, Integer> as opposed to a Map<String, KeyValue<String, Integer>> so not going to accept my solution - it's here purely just to show how I've got something working (for now).