วิธีที่สะอาดที่สุดในการสร้างสตริง SQL ใน Java


108

ฉันต้องการสร้างสตริง SQL เพื่อจัดการฐานข้อมูล (อัปเดตลบแทรกเลือกประเภทของสิ่งนั้น) - แทนที่จะใช้วิธีการต่อสายอักขระที่น่ากลัวโดยใช้ "+" และเครื่องหมายคำพูดนับล้านซึ่งไม่สามารถอ่านได้ดีที่สุด - ที่นั่น จะต้องเป็นวิธีที่ดีกว่า

ฉันคิดว่าจะใช้ MessageFormat - แต่มันควรจะใช้สำหรับข้อความของผู้ใช้แม้ว่าฉันคิดว่ามันจะทำงานได้ดี แต่ฉันคิดว่าน่าจะมีอะไรที่สอดคล้องกับการดำเนินการประเภท SQL ในไลบรารี java sql มากกว่า

Groovy จะดีหรือไม่?

คำตอบ:


76

ก่อนอื่นให้พิจารณาใช้พารามิเตอร์การค้นหาในคำสั่งที่เตรียมไว้:

PreparedStatement stm = c.prepareStatement("UPDATE user_table SET name=? WHERE id=?");
stm.setString(1, "the name");
stm.setInt(2, 345);
stm.executeUpdate();

สิ่งอื่นที่สามารถทำได้คือเก็บเคียวรีทั้งหมดไว้ในไฟล์คุณสมบัติ ตัวอย่างเช่นในไฟล์ queries.properties สามารถวางแบบสอบถามด้านบน:

update_query=UPDATE user_table SET name=? WHERE id=?

จากนั้นด้วยความช่วยเหลือของคลาสยูทิลิตี้ง่ายๆ:

public class Queries {

    private static final String propFileName = "queries.properties";
    private static Properties props;

    public static Properties getQueries() throws SQLException {
        InputStream is = 
            Queries.class.getResourceAsStream("/" + propFileName);
        if (is == null){
            throw new SQLException("Unable to load property file: " + propFileName);
        }
        //singleton
        if(props == null){
            props = new Properties();
            try {
                props.load(is);
            } catch (IOException e) {
                throw new SQLException("Unable to load property file: " + propFileName + "\n" + e.getMessage());
            }           
        }
        return props;
    }

    public static String getQuery(String query) throws SQLException{
        return getQueries().getProperty(query);
    }

}

คุณอาจใช้คำถามของคุณดังนี้:

PreparedStatement stm = c.prepareStatement(Queries.getQuery("update_query"));

นี่เป็นวิธีแก้ปัญหาที่ค่อนข้างง่าย แต่ใช้ได้ผลดี


1
ฉันชอบใช้ตัวสร้าง SQL ที่สะอาดเช่นนี้: mentabean.soliveirajr.com
— TraderJoeChicago

2
ฉันขอแนะนำให้คุณใส่InputStreamด้านในของif (props == null)คำสั่งเพื่อที่คุณจะได้ไม่สร้างอินสแตนซ์เมื่อไม่จำเป็น
— SyntaxRules

64

สำหรับพล SQL ใช้jOOQ jOOQ ขณะนี้สนับสนุนSELECT, INSERT, UPDATE, DELETE, และTRUNCATE MERGEคุณสามารถสร้าง SQL ได้ดังนี้:

String sql1 = DSL.using(SQLDialect.MYSQL)  
                 .select(A, B, C)
                 .from(MY_TABLE)
                 .where(A.equal(5))
                 .and(B.greaterThan(8))
                 .getSQL();

String sql2 = DSL.using(SQLDialect.MYSQL)  
                 .insertInto(MY_TABLE)
                 .values(A, 1)
                 .values(B, 2)
                 .getSQL();

String sql3 = DSL.using(SQLDialect.MYSQL)  
                 .update(MY_TABLE)
                 .set(A, 1)
                 .set(B, 2)
                 .where(C.greaterThan(5))
                 .getSQL();

แทนที่จะได้รับสตริง SQL คุณสามารถดำเนินการได้โดยใช้ jOOQ ดู

http://www.jooq.org

(ข้อจำกัดความรับผิดชอบ: ฉันทำงานให้กับ บริษัท ที่อยู่เบื้องหลัง jOOQ)


ในหลาย ๆ กรณีจะไม่ใช่วิธีแก้ปัญหาที่ไม่ดีเนื่องจากคุณไม่สามารถปล่อยให้ dbms แยกวิเคราะห์คำสั่งล่วงหน้าด้วยค่าต่างๆสำหรับ "5", "8" ฯลฯ ได้หรือไม่? ฉันเดาว่าการรันด้วย jooq จะแก้ปัญหาได้หรือไม่?
— Vegard

@Vegard: คุณมีวิธีการควบคุมการjOOQควรทำให้ค่าผูกในการส่งออกของ SQL: jooq.org/doc/3.1/manual/sql-building/bind-values กล่าวอีกนัยหนึ่งคือคุณสามารถเลือกได้ว่าจะแสดงผล"?"หรือจะกำหนดค่าการผูกแบบอินไลน์
— Lukas Eder

1
@Vegard: ไม่มีสิ่งใดป้องกันไม่ให้คุณส่งผ่านตัวแปรไปยัง jOOQ API และสร้างคำสั่ง SQL ขึ้นมาใหม่ นอกจากนี้คุณสามารถแยกค่าการผูกตามลำดับโดยใช้jooq.org/javadoc/latest/org/jooq/Query.html#getBindValues ​​()หรือตั้งชื่อค่าการผูกตามชื่อโดยใช้jooq.org/javadoc/latest/org/jooq /Query.html#getParams () คำตอบของฉันมีเพียงตัวอย่างที่เรียบง่าย ... ฉันไม่แน่ใจว่าสิ่งนี้ตอบสนองความกังวลของคุณหรือไม่?
— Lukas Eder

2
เป็นทางออกที่มีค่าใช้จ่ายสูง
— ตัวเรียงลำดับ

1
อย่าลืมใส่ข้อความปฏิเสธความรับผิดชอบที่ระบุว่าคุณเป็น CEO ของ บริษัท ที่อยู่เบื้องหลัง jOOQ ;)
— Stephan

15

เทคโนโลยีหนึ่งที่คุณควรพิจารณาคือSQLJซึ่งเป็นวิธีฝังคำสั่ง SQL โดยตรงใน Java ตัวอย่างง่ายๆคุณอาจมีสิ่งต่อไปนี้ในไฟล์ชื่อ TestQueries.sqlj:

public class TestQueries
{
    public String getUsername(int id)
    {
        String username;
        #sql
        {
            select username into :username
            from users
            where pkey = :id
        };
        return username;
    }
}

มีขั้นตอนการคอมไพล์เพิ่มเติมเพิ่มเติมซึ่งใช้ไฟล์. sqlj ของคุณและแปลเป็น Java บริสุทธิ์ - ในระยะสั้นจะมองหาบล็อกพิเศษที่คั่นด้วย

#sql
{
    ...
}

และเปลี่ยนเป็นการเรียก JDBC มีประโยชน์หลักหลายประการในการใช้ SQLJ:

  • แยกเลเยอร์ JDBC ออกไปโดยสิ้นเชิง - โปรแกรมเมอร์ต้องคิดถึง Java และ SQL เท่านั้น
  • ตัวแปลสามารถตรวจสอบคำค้นหาของคุณสำหรับไวยากรณ์ ฯลฯ เทียบกับฐานข้อมูลในเวลาคอมไพล์
  • ความสามารถในการผูกตัวแปร Java โดยตรงในแบบสอบถามโดยใช้คำนำหน้า ":"

มีการใช้งานตัวแปลสำหรับผู้ให้บริการฐานข้อมูลรายใหญ่ส่วนใหญ่ดังนั้นคุณจะสามารถค้นหาทุกสิ่งที่คุณต้องการได้อย่างง่ายดาย


อันนี้ล้าสมัยแล้วตามวิกิพีเดีย
— Zeus

1
ในขณะที่เขียน (ม.ค. 2016) SQLJ ถูกอ้างถึงใน Wikipediaว่า "ล้าสมัย" โดยไม่มีการอ้างอิงใด ๆ มันถูกทิ้งอย่างเป็นทางการหรือไม่? ถ้าใช่ฉันจะติดคำเตือนที่ด้านบนของคำตอบนี้
— Ashley Mercer

NB เทคโนโลยียังคงได้รับการสนับสนุนเช่นในรุ่นล่าสุดของ Oracle, 12c ฉันจะยอมรับว่ามันไม่ใช่มาตรฐานที่ทันสมัยที่สุด แต่ก็ยังใช้งานได้และมีประโยชน์บางอย่าง (เช่นการตรวจสอบเวลาคอมไพล์ของแบบสอบถามเทียบกับ DB) ที่ไม่มีในระบบอื่น
— Ashley Mercer

12

ฉันสงสัยว่าคุณเป็นคนชอบSquiggleหรือเปล่า นอกจากนี้ยังมีสิ่งที่มีประโยชน์มากคือjDBI แม้ว่าจะไม่ช่วยคุณในการสืบค้น


9

ฉันจะมีลักษณะที่ฤดูใบไม้ผลิ JDBC ฉันใช้เมื่อใดก็ตามที่ฉันต้องการเรียกใช้งาน SQL โดยใช้โปรแกรม ตัวอย่าง:

int countOfActorsNamedJoe
    = jdbcTemplate.queryForInt("select count(0) from t_actors where first_name = ?", new Object[]{"Joe"});

มันยอดเยี่ยมมากสำหรับการเรียกใช้งาน sql ทุกประเภทโดยเฉพาะการสืบค้น มันจะช่วยคุณแมปชุดผลลัพธ์กับวัตถุโดยไม่ต้องเพิ่มความซับซ้อนของ ORM ที่สมบูรณ์


ฉันจะรับแบบสอบถาม sql ที่ดำเนินการจริงได้อย่างไร ฉันต้องการบันทึกมัน
— kodmanyagha

5

ฉันมักจะใช้พารามิเตอร์ Named JDBC ของ Spring ดังนั้นฉันจึงสามารถเขียนสตริงมาตรฐานเช่น "select * from blah โดยที่ colX = ': someValue'"; คิดว่าน่าอ่านทีเดียว

อีกทางเลือกหนึ่งคือการจัดหาสตริงในไฟล์. sql แยกต่างหากและอ่านเนื้อหาโดยใช้วิธียูทิลิตี้

โอ้คุ้มค่ากับการดู Squill: https://squill.dev.java.net/docs/tutorial.html


ฉันคิดว่าคุณหมายถึงคุณกำลังใช้ BeanPropertySqlParameterSource? ฉันเกือบจะเห็นด้วยกับคุณคลาสที่ฉันเพิ่งพูดถึงนั้นยอดเยี่ยมเมื่อใช้ถั่วอย่างเคร่งครัด แต่อย่างอื่นฉันขอแนะนำให้ใช้ ParameterizedRowMapper ที่กำหนดเองเพื่อสร้างวัตถุ
— Esko

ไม่มาก คุณสามารถใช้ SqlParameterSource ใด ๆ กับ Named JDBC Parameters มันเหมาะกับความต้องการของฉันที่จะใช้ MapSqlParameterSource แทนที่จะเป็นพันธุ์ถั่ว ทั้งสองวิธีนี้เป็นทางออกที่ดี อย่างไรก็ตาม RowMappers จัดการกับอีกด้านหนึ่งของปริศนา SQL: เปลี่ยนผลลัพธ์ให้เป็นวัตถุ
— GaryF

4

ฉันสองคำแนะนำสำหรับการใช้ ORM เช่น Hibernate อย่างไรก็ตามมีบางสถานการณ์ที่ใช้ไม่ได้ผลดังนั้นฉันจะใช้โอกาสนี้เพื่อพูดถึงบางสิ่งที่ฉันได้ช่วยเขียน: SqlBuilderเป็นไลบรารี java สำหรับการสร้างคำสั่ง sql แบบไดนามิกโดยใช้สไตล์ "builder" มันค่อนข้างทรงพลังและยืดหยุ่นพอสมควร


4

ฉันทำงานกับแอ็พพลิเคชัน Java servlet ที่ต้องการสร้างคำสั่ง SQL แบบไดนามิกมากเพื่อวัตถุประสงค์ในการรายงาน adhoc ฟังก์ชั่นพื้นฐานของแอพนี้คือป้อนพารามิเตอร์คำขอ HTTP ที่มีชื่อจำนวนมากลงในแบบสอบถามที่เข้ารหัสไว้ล่วงหน้าและสร้างตารางผลลัพธ์ที่จัดรูปแบบไว้อย่างสวยงาม ฉันใช้ Spring MVC และเฟรมเวิร์กการฉีดพึ่งพาเพื่อจัดเก็บการสืบค้น SQL ทั้งหมดของฉันในไฟล์ XML และโหลดลงในแอปพลิเคชันการรายงานพร้อมกับข้อมูลการจัดรูปแบบตาราง ในที่สุดข้อกำหนดการรายงานก็ซับซ้อนกว่าความสามารถของเฟรมเวิร์กการแม็ปพารามิเตอร์ที่มีอยู่และฉันต้องเขียนเอง เป็นแบบฝึกหัดที่น่าสนใจในการพัฒนาและสร้างกรอบสำหรับการทำแผนที่พารามิเตอร์ที่มีประสิทธิภาพมากกว่าสิ่งอื่นใดที่ฉันสามารถหาได้

การแมปพารามิเตอร์ใหม่มีลักษณะดังนี้:

select app.name as "App", 
       ${optional(" app.owner as "Owner", "):showOwner}
       sv.name as "Server", sum(act.trans_ct) as "Trans"
  from activity_records act, servers sv, applications app
 where act.server_id = sv.id
   and act.app_id = app.id
   and sv.id = ${integer(0,50):serverId}
   and app.id in ${integerList(50):appId}
 group by app.name, ${optional(" app.owner, "):showOwner} sv.name
 order by app.name, sv.name

ความสวยงามของเฟรมเวิร์กที่ได้คือสามารถประมวลผลพารามิเตอร์คำร้องขอ HTTP ลงในแบบสอบถามได้โดยตรงด้วยการตรวจสอบประเภทที่เหมาะสมและการ จำกัด การตรวจสอบ ไม่จำเป็นต้องมีการแมปเพิ่มเติมสำหรับการตรวจสอบอินพุต ในตัวอย่างแบบสอบถามด้านบนพารามิเตอร์ที่ชื่อserverId จะถูกตรวจสอบเพื่อให้แน่ใจว่าสามารถส่งเป็นจำนวนเต็มและอยู่ในช่วง 0-50 พารามิเตอร์appIdจะถูกประมวลผลเป็นอาร์เรย์ของจำนวนเต็มโดยจำกัดความยาวไว้ที่ 50 หากฟิลด์showOwnerมีอยู่และตั้งค่าเป็น "จริง" บิตของ SQL ในเครื่องหมายคำพูดจะถูกเพิ่มลงในแบบสอบถามที่สร้างขึ้นสำหรับการแมปฟิลด์ที่เป็นทางเลือก ฟิลด์มีการแม็พประเภทพารามิเตอร์อีกมากมายรวมถึงเซ็กเมนต์เสริมของ SQL ที่มีการแม็พพารามิเตอร์เพิ่มเติม ช่วยให้การแมปแบบสอบถามมีความซับซ้อนมากที่สุดเท่าที่นักพัฒนาสามารถคิดขึ้นได้ มันยังมีการควบคุมในการกำหนดค่ารายงานเพื่อกำหนดว่าแบบสอบถามที่ระบุจะมีการแมปขั้นสุดท้ายผ่านทาง PreparedStatement หรือเพียงแค่เรียกใช้เป็นแบบสอบถามที่สร้างไว้ล่วงหน้า

สำหรับค่าคำขอ Http ตัวอย่าง:

showOwner: true
serverId: 20
appId: 1,2,3,5,7,11,13

มันจะสร้าง SQL ต่อไปนี้:

select app.name as "App", 
       app.owner as "Owner", 
       sv.name as "Server", sum(act.trans_ct) as "Trans"
  from activity_records act, servers sv, applications app
 where act.server_id = sv.id
   and act.app_id = app.id
   and sv.id = 20
   and app.id in (1,2,3,5,7,11,13)
 group by app.name,  app.owner,  sv.name
 order by app.name, sv.name

ฉันคิดว่า Spring หรือ Hibernate หรือหนึ่งในกรอบเหล่านั้นควรมีกลไกการทำแผนที่ที่มีประสิทธิภาพมากขึ้นซึ่งตรวจสอบประเภทอนุญาตประเภทข้อมูลที่ซับซ้อนเช่นอาร์เรย์และคุณสมบัติอื่น ๆ ดังกล่าว ฉันเขียนเครื่องยนต์ของฉันเพื่อจุดประสงค์เท่านั้นมันยังไม่ได้อ่านสำหรับรุ่นทั่วไป ขณะนี้ใช้งานได้กับแบบสอบถามของ Oracle เท่านั้นและรหัสทั้งหมดเป็นของ บริษัท ขนาดใหญ่ สักวันฉันอาจใช้ความคิดและสร้างกรอบโอเพนซอร์สใหม่ แต่ฉันหวังว่าหนึ่งในผู้เล่นรายใหญ่ที่มีอยู่จะรับมือกับความท้าทายนี้


3

ทำไมคุณถึงต้องการสร้าง sql ทั้งหมดด้วยมือ? คุณเคยดู ORM เช่น Hibernate หรือไม่ทั้งนี้ขึ้นอยู่กับโครงการของคุณว่าอาจทำสิ่งที่คุณต้องการได้อย่างน้อย 95% ทำด้วยวิธีที่สะอาดกว่าจากนั้นใช้ SQL ดิบและหากคุณต้องการได้รับประสิทธิภาพบิตสุดท้ายคุณสามารถสร้าง แบบสอบถาม SQL ที่ต้องปรับแต่งด้วยมือ


3

คุณสามารถดูที่ MyBatis ( www.mybatis.org ) ช่วยให้คุณเขียนคำสั่ง SQL นอกโค้ด java ของคุณและแมปผลลัพธ์ sql ลงในวัตถุ java ของคุณเหนือสิ่งอื่นใด


3

Google มีห้องสมุดที่เรียกว่าห้อง Persitence ห้องสมุดซึ่งมีวิธีทำความสะอาดมากของการเขียน SQL สำหรับ Android Appsพื้นชั้น abstraction กว่าต้นแบบฐานข้อมูล SQLite Bellow เป็นข้อมูลโค้ดสั้น ๆ จากเว็บไซต์ทางการ:

@Dao
public interface UserDao {
    @Query("SELECT * FROM user")
    List<User> getAll();

    @Query("SELECT * FROM user WHERE uid IN (:userIds)")
    List<User> loadAllByIds(int[] userIds);

    @Query("SELECT * FROM user WHERE first_name LIKE :first AND "
           + "last_name LIKE :last LIMIT 1")
    User findByName(String first, String last);

    @Insert
    void insertAll(User... users);

    @Delete
    void delete(User user);
}

มีตัวอย่างเพิ่มเติมและเอกสารประกอบที่ดีกว่าในเอกสารอย่างเป็นทางการสำหรับไลบรารี

นอกจากนี้ยังเป็นหนึ่งเรียกว่า MentaBean ซึ่งเป็นJava ออม มีคุณสมบัติที่ดีและดูเหมือนจะเป็นวิธีการเขียน SQL ที่ค่อนข้างง่าย


ตามเอกสารเกี่ยวกับห้อง : Room provides an abstraction layer over SQLite to allow fluent database access while harnessing the full power of SQLite. ดังนั้นจึงไม่ใช่ไลบรารี ORM ทั่วไปสำหรับ RDBMS มีไว้สำหรับแอป Android เป็นหลัก
— RafiAlhamd

2

อ่านไฟล์ XML

คุณสามารถอ่านได้จากไฟล์ XML ง่ายต่อการบำรุงรักษาและใช้งานได้ มีตัวแยกวิเคราะห์ STaX, DOM, SAX มาตรฐานที่พร้อมใช้งานเพื่อให้โค้ดไม่กี่บรรทัดใน java

ทำสิ่งต่างๆได้มากขึ้นด้วยแอตทริบิวต์

คุณสามารถมีข้อมูลเชิงความหมายพร้อมแอตทริบิวต์บนแท็กเพื่อช่วยทำสิ่งต่างๆกับ SQL ได้มากขึ้น อาจเป็นชื่อวิธีการหรือประเภทคิวรีหรืออะไรก็ได้ที่ช่วยให้คุณเขียนโค้ดน้อยลง

บำรุงรักษา

คุณสามารถใส่ xml นอกโถและดูแลรักษาได้อย่างง่ายดาย ประโยชน์เช่นเดียวกับไฟล์คุณสมบัติ

การแปลง

XML สามารถขยายและแปลงเป็นรูปแบบอื่นได้ง่าย

ใช้กรณี

Metamug ใช้ xml เพื่อกำหนดค่าไฟล์รีซอร์ส REST ด้วย sql


คุณสามารถใช้ yaml หรือ json ได้หากต้องการ ดีกว่าเก็บในไฟล์คุณสมบัติธรรมดา
— ตัวเรียงลำดับ

คำถามคือจะสร้าง SQL ได้อย่างไร ในการสร้าง SQL หากคุณจำเป็นต้องใช้ XML, Parser, Validation และอื่น ๆ มันมีภาระมากเกินไป ความพยายามในช่วงแรก ๆ ส่วนใหญ่ที่เกี่ยวข้องกับ XML ในการสร้าง SQL นั้นถูกหันไปใช้ Annotation คำตอบที่ได้รับการยอมรับโดยPiotr Kochanskiเป็นง่ายและสง่างามและตรงประเด็น - แก้ปัญหาและการบำรุงรักษา หมายเหตุ: ไม่มีวิธีอื่นในการรักษา SQL ที่ดีกว่าในภาษาอื่น
— RafiAlhamd

ฉันลบความคิดเห็นก่อนหน้านี้I don't see a reason to make use of XML. เนื่องจากไม่สามารถแก้ไขได้
— RafiAlhamd

1

หากคุณใส่สตริง SQL ในไฟล์คุณสมบัติแล้วอ่านว่าคุณสามารถเก็บสตริง SQL ไว้ในไฟล์ข้อความธรรมดาได้

นั่นไม่ได้แก้ปัญหาประเภท SQL แต่อย่างน้อยก็ทำให้การคัดลอกและวางจาก TOAD หรือ sqlplus ง่ายขึ้นมาก


0

คุณจะเชื่อมต่อสตริงได้อย่างไรนอกเหนือจากสตริง SQL แบบยาวใน PreparedStatements (ซึ่งคุณสามารถจัดเตรียมไว้ในไฟล์ข้อความและโหลดเป็นทรัพยากรได้อย่างง่ายดาย) ที่คุณแบ่งออกเป็นหลายบรรทัด

คุณไม่ได้สร้างสตริง SQL โดยตรงใช่หรือไม่? นั่นเป็นข้อห้ามที่ใหญ่ที่สุดในการเขียนโปรแกรม โปรดใช้ PreparedStatements และระบุข้อมูลเป็นพารามิเตอร์ ช่วยลดโอกาสของ SQL Injection ได้อย่างมาก


แต่ถ้าคุณไม่ได้เปิดเผยหน้าเว็บสู่สาธารณะ - SQL Injection เป็นปัญหาที่เกี่ยวข้องหรือไม่?
— Vidar

4
SQL Injection มีความเกี่ยวข้องเสมอเนื่องจากอาจเกิดขึ้นโดยบังเอิญและโดยเจตนา
— sleske

1
@Vidar - คุณอาจไม่ได้เปิดเผยหน้าเว็บต่อสาธารณะในตอนนี้แต่แม้แต่โค้ดที่ "เสมอ" จะอยู่ภายในก็มักจะได้รับการเปิดเผยจากภายนอกบางอย่างในบางประเด็น และทั้งเร็วกว่าและปลอดภัยกว่าในการทำครั้งแรกในรอบแรกมากกว่าที่จะต้องตรวจสอบโค้ดเบสทั้งหมดเพื่อหาปัญหาในภายหลัง ...
— Andrzej Doyle

4
แม้แต่ PreparedStatement ก็ต้องสร้างจาก String ไม่ใช่เหรอ?
— Stewart

ใช่ แต่การสร้าง PreparedStatement จาก String นั้นปลอดภัยตราบใดที่คุณสร้าง PreparedStatement ที่ปลอดภัย คุณอาจจะต้องเขียนคลาส PreparedStatementBuilder เพื่อสร้างมันขึ้นมาเพื่อซ่อนความยุ่งเหยิงของการเชื่อมสิ่งต่างๆเข้าด้วยกัน
— JeeBee
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.