Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

When insert a list or array,resolve the useGeneratedKeys error. #324

Closed
wants to merge 1 commit into from
Closed

Conversation

abel533
Copy link
Contributor

@abel533 abel533 commented Dec 31, 2014

When insert a list or array,resolve the useGeneratedKeys error.

The Sample:

Mapper.xml File:

<insert id="insertAll" keyProperty="id" useGeneratedKeys="true">
  insert into country(countryname,countrycode)
  VALUES
  <foreach collection="list" item="country" separator=",">
    (#{country.countryname},#{country.countrycode})
  </foreach>
</insert>

The Java code File:

List<Country> countryList = new ArrayList<Country>();
Country country = new Country();
country.setCountryname("Name1");
country.setCountrycode("N1");
countryList.add(country);

country = new Country();
country.setCountryname("Name2");
country.setCountrycode("N2");
countryList.add(country);

country = new Country();
country.setCountryname("Name3");
country.setCountrycode("N3");
countryList.add(country);

country = new Country();
country.setCountryname("Name4");
country.setCountrycode("N4");
countryList.add(country);
//ERROR:
//Caused by: org.apache.ibatis.binding.BindingException: 
//Parameter 'id' not found. Available parameters are [list]
int result = sqlSession.insert("insertAll",countryList);

for (Country country1 : countryList) {
    System.out.println(country1.getId());
}

Solution

Add a method to handle the parameters:

private Collection<Object> getParameters(Object parameter)

Add a first Key keys,and then developer can use keys to specify the reveive param.

If not specif,find listorarray.If not existed,use the parameter only.

Note:use Collectionsupport more than List,like Set.

@PavelTurk
Copy link

I use now mybatis 3.3.0 and get the error - Parameter not found (http://stackoverflow.com/questions/28453475/mybatis-getting-id-from-inserted-array-of-object-returns-error) I've read code from master branch but I don't see there your patches. Can you say 1)was you code accepted by mybatis developers and if yes why there is no you code 2)can I use you patch for version 3.3.0 to solve my problem?

@pagehelper
Copy link

@PashaTurok You can see the code here:

this Issue Code: https://github.com/mybatis/mybatis-3/pull/324/files

Source code:Jdbc3KeyGenerator

In class Jdbc3KeyGenerator, there is an obvious error.

At line 45:

List<Object> parameters = new ArrayList<Object>();
parameters.add(parameter);
processBatch(ms, stmt, parameters);

Create a new List,and only add one value parameter,then call processBatch.

In processBatchmethod at line 60:

for (Object parameter : parameters) {

the parameters has only on one Object.

Judging from the design, it can support insert List and write-back id.

I don't know why not merge this request.

If you need to, you can refer to this class Jdbc3KeyGenerator - support insert List and write-back id

If you use it,in the following code:

public void createCore(@Param("cores")List<Object> cores);  

you need remove @Param then is will be the default key "list",or you can use key "keys" to replace the "cores".

@pagehelper
Copy link

@emacarron We can discuss this issue.

How to solve the problem of insert List and write-back id?

@PavelTurk
Copy link

@pagehelper Thank you very much!!! Your solution really solved my problem! Can this be applied for different databases? I mean mysq,db2,ms,oracle?

@PavelTurk
Copy link

@pagehelper One more question - you say "you need remove @param then is will be the default key "list",or you can use key "keys" to replace the "cores"' - this is rule, as I understand for all inserts. Ok, it's not problem. But should I follow this rule and for update?

@Condor70
Copy link

@pagehelper:
Your solution for working with map parameters feels wrong to me. Instead of looking for a "list" or "array" key, the code should examine the keyProperty value (e.g. it could be specified as "list[].id" or maybe just as "array.id").

@pagehelper
Copy link

@PashaTurok When you use the useGeneratedKeys property.MyBatis will use Jdbc3KeyGenerator, but useGeneratedKeys doesn't support all kinds of databases.

In MyBatis document:

useGeneratedKeys (insert and update only) This tells MyBatis to use the JDBC getGeneratedKeys method to retrieve keys generated internally by the database (e.g. auto increment fields in RDBMS like MySQL or SQL Server). Default: false

Full document:http://mybatis.github.io/mybatis-3/sqlmap-xml.html

@pagehelper
Copy link

@Condor70 Here is an example.You can refer to it.

A simple table,named country:

CREATE TABLE `country` (
  `Id` int(11) NOT NULL AUTO_INCREMENT,
  `countryname` varchar(255) DEFAULT NULL,
  `countrycode` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`Id`)
) ENGINE=InnoDB AUTO_INCREMENT=184 DEFAULT CHARSET=utf8;

Mapper.xml:

<insert id="insertList" useGeneratedKeys="true" keyProperty="id">
  INSERT INTO country (countryname,countrycode )
  VALUES
  <foreach collection="list" item="item" separator=",">
    (#{item.countryname},#{item.countrycode})
  </foreach>
</insert>

Note:keyProperty="id" and collection="list"

A CountryList:

List<Country> countries = new ArrayList<Country>();
Country country = new Country();
country.setCountryname("cn1");
country.setCountrycode("cc1");
countries.add(country);
Country country2 = new Country();
country2.setCountryname("cn2");
country2.setCountrycode("cc2");
countries.add(country2);
Country country3 = new Country();
country3.setCountryname("cn3");
country3.setCountrycode("cc3");
countries.add(country3);

I use three ways to test Insert.

use List:

int result = sqlSession.insert("insertList", countries);
Assert.assertEquals(3, result);
for (Country c : countries) {
    Assert.assertNotNull(c.getId());
}

use Map:

Map<String, Object> map = new HashMap<String, Object>();
map.put("list",countries);
int result = sqlSession.insert("insertList", map);
Assert.assertEquals(3, result);
for (Country c : countries) {
    Assert.assertNotNull(c.getId());
}

use Array:

first modify the Mapper.xml,change collection="list" to collection="array"

int result = sqlSession.insert("insertList", countries.toArray());
Assert.assertEquals(3, result);
for (Country c : countries) {
    Assert.assertNotNull(c.getId());
}

Others

When use mapper interface,still no problems.

@Condor70
Copy link

I fully understand your suggested solution.

My problem is that you are required to name your collection "list" or "array" if you are using a parameter map.

This seems arbitrary to me.

I would prefer it if the name of the collection or array would be derived from the keyProperty.

e.g. You could use keyProperty="myObject.children.id" and MyBatis would analyse parameters.myObject.children and decide whether it is a collection, array or plain object before assigning the id(s).

@abel533
Copy link
Contributor Author

abel533 commented Feb 12, 2015

@Condor70 It's a good idea.

I think we can combine the two methods.

  1. Use `keyProperty="list[].id" form decide whether it is a collection, array or plain object
  2. Automatic processing parameters.
private Collection<Object> getParameters(Object parameter) {
    Collection<Object> parameters = null;
    if (parameter instanceof Collection) {
      parameters = (Collection) parameter;
    } else if (parameter instanceof Map) {
      Map parameterMap = (Map) parameter;
      //TODO get key from the keyProperty
      //else
      //Default key
      if (parameterMap.containsKey("list")) {
        parameters = (Collection) parameterMap.get("list");
      } else if (parameterMap.containsKey("array")) {
        parameters = Arrays.asList((Object[])parameterMap.get("array"));
      }
    }
    if (parameters == null) {
      parameters = new ArrayList<Object>();
      parameters.add(parameter);
    }
    return parameters;
  }

@abel533
Copy link
Contributor Author

abel533 commented Feb 13, 2015

I create a new pull request:#350

@cianfree
Copy link

with Mapper.java declared like:
public int saveFamilys(@param("familys") List familys) ;
my solutions like this:

private Collection<Object> getParameters(Object parameter) {
    Collection<Object> parameters = null;
    if (parameter instanceof Collection) {
        parameters = (Collection) parameter;
    } else if (parameter instanceof Map) {
        Map parameterMap = (Map) parameter;
        // Delete duplicate objects
        Set<Object> objects = new HashSet<Object>(parameterMap.values());
        parameters = buildParametersFromSet(parameters, objects);
    }
    if (parameters == null) {
        parameters = new ArrayList<Object>();
        parameters.add(parameter);
    }
    return parameters;
}

private Collection<Object> buildParametersFromSet(Collection<Object> parameters, Set<Object> objects) {
    if (objects.size() > 0) {
        parameters = new ArrayList<Object>();
        Iterator<Object> iter = objects.iterator();
        while(iter.hasNext()) {
            Object obj = iter.next();
            if (obj instanceof Collection) {    // detecting the object whether collection
                parameters.addAll((Collection) obj);
            } else if (obj.getClass().isArray()) {    // detecting the object whether array
                parameters.addAll(Arrays.asList((Object[]) obj));
            } else {
                parameters.add(obj);
            }
        }
    }
    return parameters;
}

But, there's still a question, when mapper interface has more than two parameters, set the auto increment key failed, any one has solution?

@velykov
Copy link

velykov commented Nov 28, 2015

I'm sorry, but Assert.assertNotNull(c.getId()); is not correct, it will return 0 result on getId(), try to check getId() > 0, because by default id in db is more than zero

@voidmain
Copy link

voidmain commented Feb 3, 2016

Did this fix not make it into 3.3.0? Any idea when this will be available in a release?

Or is there a way to override the KeyGenerator and use a custom one? I couldn't find anything about this in the docs.

@emacarron
Copy link
Member

We are planning a bug fix release for the 3.3.x branch and we can include
this one to it.

2016-02-03 16:47 GMT+01:00 Brian Pontarelli notifications@github.com:

Did this fix not make it into 3.3.0? Any idea when this will be available
in a release?


Reply to this email directly or view it on GitHub
#324 (comment).

@xiakunhou
Copy link

xiakunhou commented Jun 15, 2016

I updated to latest version 3.4.0, but this problem still be reproduced with mysql.

@Override
    public int insertBatch(@Param("bIndemnitys") List<BillIndemnity> bIndemnitys);
<insert id="insertBatch" parameterType="net.ebaolife.tpa.model.cal.BillIndemnity"
        useGeneratedKeys="true" keyProperty="billIndemnityId" keyColumn="bill_indemnity_id">
        INSERT INTO ta_bill_indemnity
        (
        divisional_case_id,
        divisional_case_no,
        case_bill_id
        )
        VALUES
        <foreach collection="bIndemnitys" item="model" separator=",">
            (
            #{model.divisionalCaseId},
            #{model.divisionalCaseNo},
            #{model.caseBillId}
            )
        </foreach>
    </insert>

@xiakunhou
Copy link

xiakunhou commented Jun 15, 2016

After debugging into source code, I found several problems:

  1. when use @Param to a list, only the first element use this as key, other elements will use other keys, for example, "param1" ..... Is this correct for @Param.
  2. Because of the above problem, the below code cannot work well
else if (parameter instanceof Map) {
      Map parameterMap = (Map) parameter;
      if (parameterMap.containsKey("collection")) {
        parameters = (Collection) parameterMap.get("collection");
      } else if (parameterMap.containsKey("list")) {
        parameters = (List) parameterMap.get("list");
      } else if (parameterMap.containsKey("array")) {
        parameters = Arrays.asList((Object[]) parameterMap.get("array"));
      }
    }

it cannot fall to any cases("collection", "list", or "array"), it just an objet. It leads to fail of this fix.

After remove @Param and use list instead of bIndemnitys in xml, all works well.

Anyone can explain this?

@xiakunhou
Copy link

@abel533 @emacarron

@abel533
Copy link
Contributor Author

abel533 commented Jun 21, 2016

@XiaokunHou default key is list or array, See DefaultSqlSession:

private Object wrapCollection(final Object object) {
    if (object instanceof List) {
      StrictMap<Object> map = new StrictMap<Object>();
      map.put("list", object);
      return map;
    } else if (object != null && object.getClass().isArray()) {
      StrictMap<Object> map = new StrictMap<Object>();
      map.put("array", object);
      return map;
    }
    return object;
  }

@abel533
Copy link
Contributor Author

abel533 commented Jun 21, 2016

@XiaokunHou When your method has more than one arguments, you must assign @Param("list") or @Param("array").

@xiakunhou
Copy link

@abel533 you answered my first question.

For the second problem, this fix cannot handle collections with a customized key with @Param .

@velykov
Copy link

velykov commented Oct 3, 2016

Is it fixed, bug with no id, after array is passed to insert?

@neesonqk
Copy link

I'm using latest 3.4.1, however, this issue seems still existing to me.

void insertCells(List<RawCell> list);

<insert id="insertCells" parameterType="RawCell" keyProperty="id" useGeneratedKeys="true">
        INSERT INTO cell (shiftId, date, groupId, problems, scheduleId, start, end, planned, actual, type)
        VALUES
        <foreach collection="list" item="item" index="index" open="" separator="," close="">
            (#{item.shiftId}, #{item.date},#{item.groupId},#{item.problems},#{item.scheduleId},#{item.start},#{item.end},#{item.planned},#{item.actual},#{item.type})
        </foreach>
    </insert>

RawCell:

   private int id;

   private int scheduleId;
   private int groupId;

   private int shiftId;
   private Date date;
   private String problems;

   //Shift start & end time
   private Date start;
   private Date end;

   private float planned;
   private float actual;

   private String type;

   private int employeeId;

   //getters & setters

This is the error:

Error getting generated key or setting result to parameter object. Cause: org.apache.ibatis.binding.BindingException: Parameter 'id' not found. Available parameters are [list]

@xiakunhou
Copy link

@neesonqk try to use @Param("list") in your method

@neesonqk
Copy link

neesonqk commented Nov 25, 2016

@XiaokunHou it's a bit weird since I imported the latest source code to my project rather than pulling from Maven repository, this exception's gone, but I'm sure I imported the proper 3.4.0.jar within my pom.xml.

<dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.4.1</version>
</dependency>

is that possible a wrong jar package pushed to Maven center repository? I mean in a sense?

N It works well without @param("list") annotation.

@kazuki43zoo
Copy link
Member

Hi guys, thanks for feedback!

If there are problem or some enhancement point, please create a new issue that related with this issue. (Because this enhancement has been released already)

@kazuki43zoo
Copy link
Member

fixed via #547

@together0519
Copy link

I user oracle , When insert a list ,it throw Exception.(### Cause: java.sql.SQLSyntaxErrorException: ORA-00933: )
`

    insert into hs_opt_record(
    RECO_ID,
    EWB_NO ,
    LENGTH ,
    WIDTH  ,
    HIGH   ,
    STATUS,
    SITE_ID    ,
    SITE_NAME  ,
    SITE_TYPE  ,
    CREATE_TIME,
    CREATEBY ,
    CREATEBY_ID
    )
    SELECT seq_opt_record.nextval ,a.* FROM (
    <foreach collection="list" index="index" item="record" separator="union all">

      select
        #{record.ewbNo},
        #{record.length},
        #{record.width},
        #{record.high},
        1,
        #{record.siteId},
        #{record.siteName},
        10,
        sysdate,
        #{record.createBy},
        #{record.createById} from dual
    </foreach>
    ) a
</insert>`

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet