In try to create a spreadsheet that is generated from multiple queries, the following code only generates one row of data and not the entire spreadsheet:
<cfset filenametouse = 'Usage_Report' />
<cfset theDir = GetDirectoryFromPath(GetCurrentTemplatePath()) />
<cfset theFile = theDir & filenametouse & ".xls" />
<cflock name="fileActionSentItems" type="exclusive" timeout="30" throwontimeout="true">
<cfset SpreadsheetObj = spreadsheetNew()>
<cfset fcol = {}>
<cfset fcol.dataformat = "#">
<cfset SpreadsheetAddRow(SpreadsheetObj, "Part Number, Description, Allocated, On Hand, Pending Receipt, Job Count, Qty Needed, Qty Issued, Order Count, Qty Ordered, Qty Shipped")>
<cfoutput>
<cfset SpreadsheetAddRows(SpreadsheetObj,"#getParts.partnum#, #getParts.partdescription#, #getParts.allocated#, #getParts.onhand#, #receiptdata.recqty#, #jobdata.JobCount#, #jobdata.QtyNeeded#, #jobdata.qtySent#, #orderdata.ordercount#, #orderdata.ordered#, #orderdata.shipqty#")>
</cfoutput>
<cfset SpreadsheetFormatColumn(SpreadsheetObj,fcol,11)>
<cfspreadsheet action="write" filename="#theFile#" name="SpreadsheetObj" sheetname="Sheet1" overwrite="true" />
</cflock>
The spreadsheetAddRows isn't creating the data to populate the rows. What am I not doing correctly?
You need to pass in the query object instead of a single row of data.
<cfset SpreadsheetAddRows(SpreadsheetObj, getParts) >
As an aside, you probably do not need to lock that whole section. If the lock is intended to prevent concurrent file access, you only need to lock the code writing the sheet to disk. (Depending on your needs, you might also use a more granular name. But that is just a guess.)
You're not looping through the query to assign more than one row. Because you don't specify what position the values should come from, they're being assigned from the first record in each query.
Create another query containing everything you want in the spreadsheet with queryNew()
<cfset newQuery = queryNew("partNo,desc,allocated,onHand,rendingReceipt,jobCount,qtyNeeded,qtyIssued,orderCount,qtyOrdered,qtyShipped","varchar,varchar,varchar,varchar,varchar,varchar,integer,integer,integer,integer,integer,integer,integer,integer") />
... etc, assigning cells as you go. When you have a complete query object, then you can add that to spreadsheetAddRows(spreadsheetObj,newQuery) />
If you find the spreadsheet creation to be too slow, you may want to check out POI Utility at http://www.bennadel.com/projects/poi-utility.htm Not as configurable as the spreadsheet options, but faster for some work loads.
Related
I'm using Ben Nadel's Reading Excel Files With ColdFusion And POI codes at https://www.bennadel.com/blog/472-reading-excel-files-with-coldfusion-and-poi.htm to read my excel file.
With his example codes I can read the excel file when rows in my excel are made uniform. Ben mentioned about this in his comment. But my excel however does not always has uniform rows. Some has birth date, some does not have gender, etc.
Ben Nadel's codes produce error once it hits the empty cell. It says, the
objCell variable does not exist. The following code crashes:
<cfset objCell = objRow.GetCell(JavaCast( "int", intCell )) />
Does anyone has an example and do not mind sharing the codes that can also read empty cells when looping?
I'm pasting Ben's codes here:
<!---
Create the Excel file system object. This object is
responsible for reading in the given Excel file.
--->
<cfset objExcelFileSystem = CreateObject(
"java",
"org.apache.poi.poifs.filesystem.POIFSFileSystem"
).Init(
CreateObject(
"java",
"java.io.FileInputStream"
).Init(
ExpandPath( "./jenna_jameson.xls" )
)
) />
<!---
Get the workbook from the Excel file system object that
we just created. Remember, the workbook contains the
Excel sheets that have our data.
--->
<cfset objWorkBook = CreateObject(
"java",
"org.apache.poi.hssf.usermodel.HSSFWorkbook"
).Init(
objExcelFileSystem
) />
<!---
For this demo, we are only interested in reading in the
data from the first sheet. Remember, since Java is zero-
based, not one-based like ColdFusion, the first Excel
sheet is at index ZERO (not ONE).
--->
<cfset objSheet = objWorkBook.GetSheetAt(
JavaCast( "int", 0 )
) />
<!---
We are going to build a ColdFusion query that houses the
Excel data, but we don't know anything about the data
just yet. So, just create the place holder for the query
and then we will add to it when we have more information.
--->
<cfset qCell = "" />
<!---
Get the Excel sheet's row iterator. This appears to be some
sort of implementation of the Java class java.util.TreeMap,
but I don't know much about that. What I do know, is that
this will allow us to loop over the rows in the Excel file
until there are no more to loop over. The interface for it
looks like the standard iterator interface.
--->
<cfset objRowIterator = objSheet.rowIterator() />
<!---
User the row iterator to loop over all the physical rows in
the Excel sheet. This condition checks to see if we have a
row to read in. At this point, the iterator is NOT pointing
at a valid Excel data row.
--->
<cfloop condition="objRowIterator.HasNext()">
<!---
We have determined that we have a valid row to read.
Now, move the iterator to point to this valid row.
--->
<cfset objRow = objRowIterator.Next() />
<!---
Get the number of physical cells in this row. While I
think that this can possibly change from row to row,
for the purposes of this demo, I am going to assume
that all rows are uniform and that this row is a model
of how the rest of the data will be displayed.
--->
<cfset intCellCount = objRow.GetPhysicalNumberOfCells() />
<!---
Check to see if the query variable we have it actually
a query. If we have not done anything to it yet, then
it should still just be a string value (Yahoo for
dynamic typing!!!). If that is the case, then let's use
this first data row to set up the query object.
--->
<cfif NOT IsQuery( qCell )>
<!---
Create an empty query. Doing it this way creates a
query with neither column nor row values.
--->
<cfset qCell = QueryNew( "" ) />
<!---
Now that we have an empty query, we are going to
loop over the cells COUNT for this data row and for
each cell, we are going to create a query column
of type VARCHAR. I understand that cells are going
to have different data types, but I am chosing to
store everything as a string to make it easier.
--->
<cfloop index="intCell" from="0" to="#(intCellCount - 1)#"
step="1">
<!---
Add the column. Notice that the name of the
column is the text "column" plus the column
index. I am starting my column indexes at ONE
rather than ZERO to get it back into a more
ColdFusion standard notation.
--->
<cfset QueryAddColumn(qCell,"column#(intCell + 1)#",
"CF_SQL_VARCHAR",ArrayNew( 1 )) />
</cfloop>
</cfif>
<!---
ASSERT: Whether we are on our first Excel data row or
our Nth data row, at this point, we have a ColdFusion
query object that has the proper columns defined.
--->
<!---
Add a row to the query so that we can store this row's
data values.
--->
<cfset QueryAddRow( qCell ) />
<!--- Loop over the cells in this row to find values. --->
<cfloop index="intCell" from="0" to="#(intCellCount - 1)#"
step="1">
<!---
When getting the value of a cell, it is important
to know what type of cell value we are dealing
with. If you try to grab the wrong value type,
an error might be thrown. For that reason, we must
check to see what type of cell we are working with.
These are the cell types and they are constants
of the cell object itself:
0 - CELL_TYPE_NUMERIC
1 - CELL_TYPE_STRING
2 - CELL_TYPE_FORMULA
3 - CELL_TYPE_BLANK
4 - CELL_TYPE_BOOLEAN
5 - CELL_TYPE_ERROR
--->
<!--- Get the cell from the row object. --->
----- **When it hit an empty cell CF throws error** ---
<cfset objCell = objRow.GetCell(JavaCast( "int", intCell)) />
<!--- Get the type of data in this cell. --->
<cfset objCellType = objCell.GetCellType() />
<!---
Get teh value of the cell based on the data type.
The thing to worry about here is cell forumlas and
cell dates. Formulas can be strange and dates are
stored as numeric types. For this demo, I am not
going to worry about that at all. I will just grab
dates as floats and formulas I will try to grab as
numeric values.
--->
<cfif (objCellType EQ objCell.CELL_TYPE_NUMERIC)>
<!---
Get numeric cell data. This could be a
standard number, could also be a date value.
I am going to leave it up to the calling
program to decide.
--->
<cfset objCellValue = objCell.GetNumericCellValue() />
<cfelseif (objCellType EQ objCell.CELL_TYPE_STRING)>
<cfset objCellValue = objCell.GetStringCellValue() />
<cfelseif (objCellType EQ objCell.CELL_TYPE_FORMULA)>
<!---
Since most forumlas deal with numbers, I am
going to try to grab the value as a number. If
that throws an error, I will just grab it as a
string value.
--->
<cftry>
<cfset objCellValue = objCell.GetNumericCellValue() />
<cfcatch>
<!---
The numeric grab failed. Try to get the
value as a string. If this fails, just
force the empty string.
--->
<cftry>
<cfset objCellValue = objCell.GetStringCellValue() />
<cfcatch>
<!--- Force empty string. --->
<cfset objCellValue = "" />
</cfcatch>
</cftry>
</cfcatch>
</cftry>
<cfelseif (objCellType EQ objCell.CELL_TYPE_BLANK)>
<cfset objCellValue = "" />
<cfelseif (objCellType EQ objCell.CELL_TYPE_BOOLEAN)>
<cfset objCellValue = objCell.GetBooleanCellValue() />
<cfelse>
<!--- If all else fails, get empty string. --->
<cfset objCellValue = "" />
</cfif>
<!---
ASSERT: At this point, we either got the cell value
out of the Excel data cell or we have thrown an
error or didn't get a matching type and just
have the empty string by default. No matter what,
the object objCellValue is defined and has some
sort of SIMPLE ColdFusion value in it.
--->
<!---
Now that we have a value, store it as a string in
the ColdFusion query object. Remember again that my
query names are ONE based for ColdFusion standards.
That is why I am adding 1 to the cell index.
--->
<cfset qCell[ "column#(intCell + 1)#" ][ qCell.RecordCount ] =
JavaCast( "string", objCellValue ) />
</cfloop>
</cfloop>
<!---
At this point, the excel data should be in a ColdFusion
query object. However, if the query did not contain any
record, then the row iterator was never launched which
mean we never actually defined a query. As one final check
just make sure we are dealing with a query.
--->
<cfif NOT IsQuery( qCell )>
<!--- Just define an empty query. --->
<cfset qCell = QueryNew( "" ) />
</cfif>
Perform below two steps to read empty cells also in ColdFusion-8:
Add a variable outside loops to represent HSSFCell. Example:
<cfset jCell = createObject("java", "org.apache.poi.hssf.usermodel.HSSFCell")>
Use function objRow.GetLastCellNum() instead of objRow.GetPhysicalNumberOfCells(). Example:
<cfset intCellCount = objRow.GetLastCellNum() />
Add a <cfif> condition to set value of variable objCellType based on existence of variable objCell. Example:
<cfset objCell = objRow.GetCell(JavaCast( "int", intCell)) />
<cfif structKeyExists(variables, "objCell")>
<cfset objCellType = objCell.GetCellType() />
<cfelse>
<cfset objCellType = jCell.CELL_TYPE_BLANK />
</cfif>
Replace variable name objCell in all references to objCell.CELL_TYPE_[type_name] with variable name created in step 1. Example:
<cfif (objCellType EQ jCell.CELL_TYPE_NUMERIC)>
I'm trying to automate processing of spreadsheets using ColdFusion 10 and CFSpreadSheet. So far I can read the file in and dump the query object without any issue.
<cfspreadsheet action="read" src="#theFile#" query="qData" headerrow="1" columns="1,8,9,11,33"/>
<cfdump var="#qData#"/>
The issue comes when I try to work with the data. If I do something like:
<cfoutput query="qData" maxrows="#qData.RecordCount#">
#qData.GROUP#<br />
#qData.DOB#<br />
</cfoutput>
I immediately get an error: "Element GROUP is undefined in QDATA."
If I dump the qData.ColumnList I get a column list:
BTBN(002),DOB ,GROUP ,MEMBER/DEPENDENT NAME ,REL
Is it the spaces and ( )s messing it up? If so, how can I get rid of those and update the column names either when reading in the file or immediately thereafter?
I initially thought it may be due to "group" being a reserved SQL keyword. (Don't try using that columnname in a query-of-queries.)
Instead of sanitizing the first row values for known values and resaving the file, you should loop through and use isValid("variablename", ColumnName) to determine if the column name is valid and then use the RenameColumn UDF to rename it. We prefer this method as it's critical that we don't modify the client's original Excel file (especially since Adobe ColdFusion has some bugs when writing files and will likely mess up other worksheets and/or formatting within the file.)
An easy way to remove illegal characters is to use rereplace:
ReReplace(NewColumnName, "[^a-zA-Z0-9!]", "", "ALL")
But you'll also need to ensure that the new column name isn't empty, starts with a letter and isn't already used for another column. If you are expecting the columns to be in a certain order, you could simply safely rename them "col_1" (or use numbering as a default fallback for any non-unique and/or illegal column names.)
Here's the RenameColumn UDF taken from this 2011 blog post:
http://www.neiland.net/blog/article/using-java-to-rename-columns-in-a-coldfusion-query-object/
<cffunction name="renameColumn" access="public" output="false" returntype="query" hint="Uses java to rename a given query object column">
<cfargument name="queryObj" required="true" type="query">
<cfargument name="oldColName" required="true" type="string">
<cfargument name="newColName" required="true" type="string">
<!--- Get an array of the current column names --->
<cfset var colNameArray = queryObj.getColumnNames()>
<cfset var i = 0>
<!--- Loop through the name array and try match the current column name with the target col name--->
<cfif arrayLen(colNameArray)>
<cfloop from="1" to="#arrayLen(colNameArray)#" index="i">
<!--- If we find the target col name change to the new name --->
<cfif compareNoCase(colNameArray[i],arguments.oldColName) EQ 0>
<cfset colNameArray[i] = arguments.newColName>
</cfif>
</cfloop>
</cfif>
<!--- Update the column names with the updated name array --->
<cfset queryObj.setColumnNames(colNameArray)>
<cfreturn queryObj />
</cffunction>
(In case folks do not read the comments ...)
The parenthesis and slashes will be problematic as they do not conform to the standard variable name rules. The simplest option is to use the "columnNames" attribute to specify valid column names instead. (Also, nothing to do with your question, but if you want to exclude the header row, use excludeHeaderRow="true")
<cfspreadsheet action="read" src="c:\path\file.xlsx"
query="qData"
columnNames="BTBN_002,DOB,GROUP_NAME,MEMBER_DEPEND_NAME,REL"
excludeHeaderRow="true"
headerrow="1"
... />
In most cases, you can also access invalid column names using associative array notation. However, using the "columnNames" attribute is simpler/cleaner IMO.
<cfoutput query="qData" maxrows="#qData.RecordCount#">
#qData["BTBN(002)"][currentRow]#<br />
....
</cfoutput>
SOLUTION - there were multiple spaces in the column names and ColdFusion does not tolerate that. This could probably be done better with a regex, and I'll work on that next but here's the quick and dirty solution.
<cfset colNameArray = qData.getColumnNames() />
<cfloop from="1" to="#arrayLen(colNameArray)#" index="i">
<cfset colNameArray[i] = colNameArray[i].replace(' ','') />
<cfset colNameArray[i] = colNameArray[i].replace('(','') />
<cfset colNameArray[i] = colNameArray[i].replace(')','') />
<cfset colNameArray[i] = colNameArray[i].replace('/','') />
</cfloop>
<cfset qData.setColumnNames(colNameArray) />
I am trying to retrieve and store ID's for each item retrieved from my table, so I can use these ID's later. I tried nesting the queries, but this didn't, work. Here is my first query:
<CFQUERY datasource="MyDSN" name="MAIN2"> SELECT * from order_items where orderID= #orderID#</CFQUERY>
Now, if I output this query it displays, 1 and 117 which are the two ID's I need.
My next query is:
<CFQUERY datasource="MyDSN" name="MAIN3">select c.catalogueID,
c.product_name,
c.product_price,
c.description,
p.productID
from products p
join product_catalogue c on c.catalogueid = p.catalogueid
where p.productid = "#productID#"</CFQUERY>
But it is telling me that productID is not defined, it is obviously empty. I am just getting started using ColdFusion, so I am not sure the best way to store the values I need so I use them again. I also need to loop the second query to run for each ID 1 and 117, so twice.
Any suggestions on how to accomplish this would be greatly appreciated.
Thanks
My basic rule is that if I find myself using queries to create other queries or looping over a query to execute other queries; it is time to consider combining the queries.
I'm not sure what field you are using in the MAIN2 query to feed the MAIN3 query. So, I put in "productID" in the query below. You may have to change it to fit your field name.
<CFQUERY datasource="MyDSN" name="MAIN3">select c.catalogueID,
c.product_name,
c.product_price,
c.description,
p.productID
from products p
join product_catalogue c on c.catalogueid = p.catalogueid
where p.productid IN (SELECT DISTINCT productID from order_items where orderID= <cfqueryparam value="#orderID#" cfsqltype="CF_SQL_INTEGER">)
</CFQUERY>
You could also change this query to utilize a "join" to connect [order_items] to the query.
Lastly, you should use the <cfqueryparam> tag for the where clauses; this helps protect your query from sql injection attacks.
Whenever I'm caching data for use later, I tend to ask myself how I'll be using that data, and whether it belongs in another data type rather than query.
For instance, if I'm wanting a bunch of data that I'm likely to access via ID, I can create a structure where the key is the ID, and the data is another structure of a dataset. Then I'll save this structure in application scope and only refresh it when it needs to be. This is zippy fast and so much easier to grab with
rather than querying for it every time. This is especially useful when the query that creates the original data set is kind of a resource hog with lots of joins, sub-queries, magical cross-db stored procedures, but the datasets returns are actually fairly small.
So creating your products structure would look something like this:
<CFQUERY datasource="MyDSN" name="MAIN3">
SELECT
c.catalogueID,
c.product_name,
c.product_price,
c.description,
p.productID
FROM products p
JOIN product_catalogue c
ON c.catalogueid = p.catalogueid
WHERE p.productid = <cfqueryparam value="#ProductID#" cfsqltype="cf_sql_integer">
</CFQUERY>
<cfset products = structNew() />
<cfset item = structNew() />
<cfloop query="MAIN3">
<cfif NOT structKeyExists(products, productID)>
<cfset item = structNew() />
<cfset item.catalogueID = catalogueID />
<cfset item.product_name = product_name />
<cfset item.product_price = product_price />
<cfset item.description = description />
<cfset products[productID] = structCopy(item) />
</cfif>
</cfloop>
<cfset application.products = structCopy(products) />
We have a function to pull RSS feeds and display the post on a ColdFusion page using this code:
<cfset rssUrl = "rss1">
<cffeed action="read" source="#rssUrl#" query="fitness" properties="info">
<cfset rssUrl2 = "rss2">
<cffeed action="read" source="#rssUrl2#" query="nutrition" properties="info">
<cfif #fitness.PUBLISHEDDATE# gt #nutrition.PUBLISHEDDATE#>
<cfset entries="fitness">
<cfelse>
<cfset entries="nutrition">
</cfif>
Output done via:
<cfoutput query="#entries#">
Problem is, the RSS feed has several posts and we only want to show one. Any thoughts on how to get it to pull and display only the most recent post? (We want the feed to have multiple posts, so right now our non ideal solution is to set maximum posts per feed to 1)
cfoutput/query=".." will go over an entire query. If you only want to do the first row, use:
<cfoutput>
Title from row 1: #somequery.title[1]#
</cfoutput>
Basically - array notation on the column. Make sense?
There's nothing wrong with Ray's answer, but here are some other options.
<cfoutput query="#entries#" maxrows="1">
Offers the least disruption to your existing code and, should you decide to change the number of rows displayed (like, via a user setting) it's an easy change.
OR
If you copy the query object rather than the query name (which isn't actually a copy but a copy by reference)
<cfset entries = fitness>
instead of
<cfset entries = "fitness">
you can do this
<cfoutput>
#entries.columnName1#
#entries.columnName2#
<!--- etc. --->
</cfoutput>
which will, by default, display only the first row of the query.
I'm trying to create a function to create csv files from queries. After I run the query, I'm looping through it and appending each row's fields to a StringBuffer object. To that end, I'm putting the column names into an array:
<cfset indexes = #ListToArray(Arguments.header)# />
where the argument is currently a string like:
"col1, col2, col3...."
I've verified that both the query and the array are what they should be by dumping.
The trouble comes when looping through the query results. Given:
<cfset indexes_length = #ArrayLen(indexes)# />
<cfloop query="query_for_csv">
<cfloop index="i" from="1" to="#indexes_length#">
<cfset attr = #indexes[i]# />
<cfset sbOutput.Append(
"#query_for_csv[attr][query_for_csv.CurrentRow]#") />
</cfloop>
</cfloop>
Only the first value of the first row is output before I get the error message:
[Table (rows 10 columns col1, col2, col3):
[col1: coldfusion.sql.QueryColumn#6f731eba]
[col2: coldfusion.sql.QueryColumn#6ee67e7f]
[col3: coldfusion.sql.QueryColumn#5c6647cb]
is not indexable by col2
If I replace the variable #attr# with the literal "col2":
#query_for_csv['col2'][query_for_csv.CurrentRow]#
then the loop sails through with no problem, and spits out all the values indexed by 'col2'. Any ideas?
I would guess it's the spaces in your header list that is the problem, so probably this would work:
<cfset attr = trim(indexes[i]) />
However, since you're not using them, you probably don't need that and can just do this...
<cfloop query="QueryName">
<cfloop index="CurCol" list=#QueryName.ColumnList# >
<cfset sbOutput.Append(
QueryName[CurCol][QueryName.CurrentRow]
)/>
</cfloop>
</cfloop>
p.s.
You'll note here that there's only one pair of hashes - there only needs to be one pair in your original code snippets too (in the to attribute) - the rest are unnecessary noise.
As has already been said before, try to avoid spaces before or after a list element.
In case you want to compare notes, check out the approach Ben Nadel chose to implement such a Query2CSV converter: http://www.bennadel.com/blog/1239-Updated-Converting-A-ColdFusion-Query-To-CSV-Using-QueryToCSV-.htm